#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { std::string fixtureBytes(const std::string& fixtureID, const std::string& file) { return util::read_file("particle-fixtures/" + fixtureID + "/" + file); } std::string localMetadata(const int index) { return "{\"schema\":\"maplibre-particle-field/v1\",\"fieldId\":\"production-t" + std::to_string(index) + "\",\"fieldType\":\"WIND\",\"displayDomain\":\"GLOBAL\",\"generation\":" + std::to_string(index + 1) + ",\"validTime\":" + std::to_string(1704067200000ULL + index * 3600000ULL) + ",\"dataVersion\":" + std::to_string(index + 1) + ",\"bounds\":{\"west\":-180,\"south\":-85,\"east\":180,\"north\":85},\"grid\":{\"width\":16,\"height\":16},\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"noData\":-9999},\"payload\":{\"byteLength\":2048}}"; } } // namespace using namespace mbgl; using SourceType = mbgl::style::SourceType; class SourceTest { public: util::SimpleIdentity uniqueID; util::RunLoop loop; std::shared_ptr fileSource = std::make_shared(); StubStyleObserver styleObserver; StubRenderSourceObserver renderSourceObserver; Transform transform; TransformState transformState; AnnotationManager annotationManager{style}; std::shared_ptr imageManager = ImageManager::create(); std::shared_ptr glyphManager = std::make_shared(); gfx::DynamicTextureAtlasPtr dynamicTextureAtlas; TaggedScheduler threadPool; Style style; TileParameters tileParameters(MapMode mapMode = MapMode::Continuous) { return {.pixelRatio = 1.0, .debugOptions = MapDebugOptions(), .transformState = transformState, .fileSource = fileSource, .mode = mapMode, .annotationManager = annotationManager.makeWeakPtr(), .imageManager = imageManager, .glyphManager = glyphManager, .prefetchZoomDelta = 0, .threadPool = threadPool, .dynamicTextureAtlas = dynamicTextureAtlas}; }; SourceTest() : threadPool(Scheduler::GetBackground(), uniqueID), style{fileSource, 1, threadPool} { // Squelch logging. Log::setObserver(std::make_unique()); transform.resize({512, 512}); transform.jumpTo(CameraOptions().withCenter(LatLng()).withZoom(0.0)); transformState = transform.getState(); } ~SourceTest() { threadPool.waitForEmpty(); } void run() { loop.run(); } void end() { loop.stop(); } }; TEST(Source, LoadingFail) { SourceTest test; test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("url", resource.url); Response response; response.error = std::make_unique(Response::Error::Reason::Other, "Failed by the test case"); return response; }; test.styleObserver.sourceError = [&](Source& source, std::exception_ptr error) { EXPECT_EQ("source", source.getID()); EXPECT_EQ("Failed by the test case", util::toString(error)); test.end(); }; VectorSource source("source", "url"); source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); test.run(); } TEST(Source, RasterArrayRenderItem) { SourceTest test; style::RasterArraySource source{"source", "fixture", "metadata", "field"}; particle::ParticleFieldSnapshot field; auto impl = Immutable( makeMutable(source.impl(), std::move(field))); RenderRasterArraySource renderSource{impl}; renderSource.update(impl, {}, true, false, test.tileParameters()); EXPECT_TRUE(renderSource.isLoaded()); EXPECT_TRUE(renderSource.isEnabled()); TransformParameters transformParameters{test.transformState}; const MapDebugOptions debugOptions = MapDebugOptions::NoDebug; renderSource.prepare({.transform = transformParameters, .debugOptions = debugOptions, .imageManager = *test.imageManager, .sourceName = source.getID()}); auto item = renderSource.createRenderItem(); ASSERT_TRUE(item); EXPECT_EQ(item->getName(), "source"); EXPECT_FALSE(item->hasRenderPass(RenderPass::Opaque)); EXPECT_FALSE(item->hasRenderPass(RenderPass::Translucent)); } TEST(Source, RasterArraySharesCanonicalSnapshotsWithWeatherColorLayer) { style::RasterArraySource source{"source", "fixture", "metadata", "field"}; EXPECT_TRUE(source.supportsLayerType(style::RasterParticleLayer::Impl::staticTypeInfo())); EXPECT_TRUE(source.supportsLayerType(style::WeatherColorLayer::Impl::staticTypeInfo())); } TEST(Source, RasterArraySnapshotsUseProcessWideGenerations) { SourceTest test; const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json"); const auto eastField = fixtureBytes("uniform-east", "field.f32"); const auto northMetadata = fixtureBytes("uniform-north", "metadata.json"); const auto northField = fixtureBytes("uniform-north", "field.f32"); test.fileSource->sourceResponse = [&](const Resource& resource) -> Response { Response response; if (resource.url == "east-metadata") { response.data = std::make_unique(eastMetadata); } else if (resource.url == "east-field") { response.data = std::make_unique(eastField); } else if (resource.url == "north-metadata") { response.data = std::make_unique(northMetadata); } else if (resource.url == "north-field") { response.data = std::make_unique(northField); } else { ADD_FAILURE() << "Unexpected RasterArray resource: " << resource.url; } return response; }; style::RasterArraySource east{"source", "uniform-east", "east-metadata", "east-field"}; east.setObserver(&test.styleObserver); style::RasterArraySource north{"source", "uniform-north", "north-metadata", "north-field"}; north.setObserver(&test.styleObserver); uint8_t loadedSources = 0; test.styleObserver.sourceChanged = [&](Source&) { if (++loadedSources == 2) { test.end(); } }; east.loadDescription(*test.fileSource); north.loadDescription(*test.fileSource); test.run(); ASSERT_TRUE(east.impl().field); ASSERT_TRUE(north.impl().field); EXPECT_NE(east.impl().field->generation, north.impl().field->generation); } TEST(Source, RasterArrayFieldIdentityIncludesFixtureAndDataVersion) { const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 7); const auto northWithSameGeneration = particle::OfflineFixtureParticleAdapter::load("uniform-north", 7); auto eastWithNewDataVersion = east; ++eastWithNewDataVersion.dataVersion; EXPECT_TRUE(particle::fieldIdentityChanged(east, northWithSameGeneration)); EXPECT_TRUE(particle::fieldIdentityChanged(east, eastWithNewDataVersion)); EXPECT_FALSE(particle::fieldIdentityChanged(east, east)); } TEST(Source, RasterArrayGpuContentIdentityExcludesRuntimeGeneration) { const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 100); auto reloadedEast = east; reloadedEast.generation = 200; auto changed = reloadedEast; ++changed.dataVersion; EXPECT_TRUE(particle::fieldIdentityChanged(east, reloadedEast)); EXPECT_TRUE(particle::sameFieldGpuContent(east, reloadedEast)); EXPECT_FALSE(particle::sameFieldGpuContent(east, changed)); } TEST(Source, ParticleFieldDataAdapterAcceptsNeutralDescriptor) { particle::ParticleFieldDescriptor descriptor; descriptor.fieldID = "weather-service-wind-20260904T0000Z"; descriptor.fieldType = "WIND"; descriptor.displayDomain = "GLOBAL"; descriptor.generation = 1; descriptor.width = 1; descriptor.height = 1; descriptor.byteLength = 2 * sizeof(float); float values[] = {1.0f, 2.0f}; std::vector bytes(sizeof(values)); std::memcpy(bytes.data(), values, sizeof(values)); const auto result = particle::ParticleFieldDataAdapter::decode(descriptor, bytes, 1); ASSERT_TRUE(result); ASSERT_TRUE(result.snapshot); EXPECT_EQ(result.snapshot->fieldID, descriptor.fieldID); EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::None); } TEST(Source, ParticleFieldDataAdapterReturnsStructuredValidationErrors) { particle::ParticleFieldDescriptor descriptor; descriptor.fieldID = "field"; descriptor.fieldType = "WIND"; descriptor.displayDomain = "GLOBAL"; descriptor.generation = 1; descriptor.width = 2; descriptor.height = 1; descriptor.byteLength = 4 * sizeof(float); std::vector bytes(3 * sizeof(float)); auto result = particle::ParticleFieldDataAdapter::decode(descriptor, bytes, 1); EXPECT_FALSE(result); EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::PayloadSizeMismatch); descriptor.byteLength = bytes.size(); descriptor.encoding = "provider-packed"; result = particle::ParticleFieldDataAdapter::decode(descriptor, bytes, 1); EXPECT_FALSE(result); EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::UnsupportedFormat); } TEST(Source, LocalProductionFeedSubmitsSnapshotsWithProducerTime) { const auto field = fixtureBytes("uniform-east", "field.f32"); const std::vector payload(field.begin(), field.end()); std::vector snapshots; for (int index = 0; index < 4; ++index) { const auto result = particle::LocalParticleFieldFeed::load(localMetadata(index), payload); ASSERT_TRUE(result) << result.message; ASSERT_TRUE(result.snapshot); EXPECT_EQ(result.snapshot->generation, static_cast(index + 1)); EXPECT_EQ(result.snapshot->validTime, 1704067200000ULL + static_cast(index) * 3600000ULL); snapshots.push_back(std::move(*result.snapshot)); } for (size_t index = 1; index < snapshots.size(); ++index) { EXPECT_LT(snapshots[index - 1].validTime, snapshots[index].validTime); } SourceTest test; std::vector metadata; for (int index = 0; index < 4; ++index) metadata.push_back(localMetadata(index)); test.fileSource->sourceResponse = [&](const Resource& resource) -> Response { Response response; const auto prefix = std::string("local-metadata-"); if (resource.url.rfind(prefix, 0) == 0) { const auto index = static_cast(std::stoi(resource.url.substr(prefix.size()))); response.data = std::make_unique(metadata.at(index)); } else if (resource.url.rfind("local-field-", 0) == 0) { response.data = std::make_unique(field); } else { ADD_FAILURE() << "Unexpected local production resource: " << resource.url; } return response; }; std::vector resources; for (int index = 0; index < 4; ++index) { resources.push_back({"local-metadata-" + std::to_string(index), "local-field-" + std::to_string(index)}); } style::RasterArraySource source{"local-production", std::move(resources)}; source.setObserver(&test.styleObserver); test.styleObserver.sourceChanged = [&](Source&) { if (source.impl().timeline) test.end(); }; source.loadDescription(*test.fileSource); test.run(); ASSERT_TRUE(source.impl().timeline); EXPECT_EQ(source.impl().timeline->frame().state, style::RasterArrayPlaybackState::Ready); EXPECT_EQ(source.impl().timeline->frame().t0->validTime, snapshots[0].validTime); EXPECT_EQ(source.impl().timeline->frame().t1->validTime, snapshots[1].validTime); EXPECT_TRUE(source.controlRuntimeTimeline(0)); } TEST(Source, LocalProductionFeedRejectsMissingGeneration) { const auto field = fixtureBytes("uniform-east", "field.f32"); auto metadata = localMetadata(0); const auto position = metadata.find("\"generation\":1"); ASSERT_NE(position, std::string::npos); metadata.replace(position, std::string("\"generation\":1").size(), "\"generation\":0"); const std::vector payload(field.begin(), field.end()); const auto result = particle::LocalParticleFieldFeed::load(metadata, payload); EXPECT_FALSE(result); EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::InvalidTimestampOrIdentity); } TEST(Source, CanonicalFieldUsesEastwardUAndNorthwardV) { const auto east = fixtureBytes("uniform-east", "field.f32"); const auto north = fixtureBytes("uniform-north", "field.f32"); const auto vortex = fixtureBytes("vortex", "field.f32"); const auto eastResult = particle::LocalParticleFieldFeed::load(localMetadata(0), std::vector(east.begin(), east.end())); const auto northResult = particle::LocalParticleFieldFeed::load(localMetadata(1), std::vector(north.begin(), north.end())); const auto vortexResult = particle::LocalParticleFieldFeed::load(localMetadata(2), std::vector(vortex.begin(), vortex.end())); ASSERT_TRUE(eastResult); ASSERT_TRUE(northResult); ASSERT_TRUE(vortexResult); ASSERT_TRUE(eastResult.snapshot); ASSERT_TRUE(northResult.snapshot); ASSERT_GE(eastResult.snapshot->velocity.size(), 2u); ASSERT_GE(northResult.snapshot->velocity.size(), 2u); EXPECT_FLOAT_EQ(eastResult.snapshot->velocity[0], 1.0f); EXPECT_FLOAT_EQ(eastResult.snapshot->velocity[1], 0.0f); EXPECT_FLOAT_EQ(northResult.snapshot->velocity[0], 0.0f); EXPECT_FLOAT_EQ(northResult.snapshot->velocity[1], 1.0f); ASSERT_TRUE(vortexResult.snapshot); EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[0], 0.9375f); EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[1], -0.9375f); EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[vortexResult.snapshot->velocity.size() - 2], -0.9375f); EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity.back(), 0.9375f); } TEST(Source, HostFieldSubmissionBuildsOneCoreOwnedTimeline) { SourceTest test; style::RasterArraySource source{"host-submission"}; source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); EXPECT_TRUE(source.loaded); EXPECT_TRUE(source.impl().hostFieldSubmission); EXPECT_FALSE(source.impl().field); const auto east = fixtureBytes("uniform-east", "field.f32"); const auto north = fixtureBytes("uniform-north", "field.f32"); const auto vortex = fixtureBytes("vortex", "field.f32"); const std::vector eastPayload(east.begin(), east.end()); const std::vector northPayload(north.begin(), north.end()); const std::vector vortexPayload(vortex.begin(), vortex.end()); EXPECT_EQ(source.submitHostField(localMetadata(0), eastPayload), style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow); EXPECT_FALSE(source.impl().field); EXPECT_EQ(source.submitHostField(localMetadata(1), northPayload), style::RasterArrayHostFieldSubmitResult::InitialWindowReady); ASSERT_TRUE(source.impl().field); ASSERT_TRUE(source.impl().nextField); ASSERT_TRUE(source.impl().timeline); EXPECT_EQ(source.impl().field->fieldID, "production-t0"); EXPECT_EQ(source.impl().nextField->fieldID, "production-t1"); EXPECT_EQ(source.impl().timeline->frame().state, style::RasterArrayPlaybackState::Ready); EXPECT_EQ(source.submitHostField(localMetadata(2), vortexPayload), style::RasterArrayHostFieldSubmitResult::AcceptedNextSnapshot); EXPECT_EQ(source.submitHostField(localMetadata(3), eastPayload), style::RasterArrayHostFieldSubmitResult::AcceptedNextSnapshot); EXPECT_EQ(source.impl().timeline->size(), 4u); EXPECT_EQ(source.impl().timeline->lastEntry().field->fieldID, "production-t3"); EXPECT_EQ(source.submitHostField(localMetadata(3), eastPayload), style::RasterArrayHostFieldSubmitResult::Duplicate); EXPECT_EQ(source.impl().timeline->size(), 4u); EXPECT_TRUE(source.controlRuntimeTimeline(4)); EXPECT_EQ(source.impl().timeline->frame().t0Index, 1u); EXPECT_TRUE(source.controlRuntimeTimeline(4)); EXPECT_EQ(source.impl().timeline->frame().t0Index, 2u); } TEST(Source, HostFieldClearAndTimestampSeekAreFailClosed) { SourceTest test; style::RasterArraySource source{"host-submission"}; source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); const auto east = fixtureBytes("uniform-east", "field.f32"); const auto north = fixtureBytes("uniform-north", "field.f32"); const std::vector eastPayload(east.begin(), east.end()); const std::vector northPayload(north.begin(), north.end()); EXPECT_EQ(source.submitHostField(localMetadata(0), eastPayload), style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow); EXPECT_TRUE(source.clearHostFields()); EXPECT_FALSE(source.impl().field); EXPECT_FALSE(source.impl().timeline); EXPECT_EQ(source.submitHostField(localMetadata(0), eastPayload), style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow); EXPECT_EQ(source.submitHostField(localMetadata(1), northPayload), style::RasterArrayHostFieldSubmitResult::InitialWindowReady); const uint64_t t0 = 1704067200000ULL; const uint64_t t1 = 1704070800000ULL; EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(t0)); EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 0.0f); EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(t0 + 1800000ULL)); EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 0.5f); EXPECT_TRUE(source.seekRuntimeTimelineToTimestamp(t1)); EXPECT_FLOAT_EQ(source.impl().timeline->frame().mix, 1.0f); EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(t0 - 1)); EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(t1 + 1)); EXPECT_TRUE(source.clearHostFields()); EXPECT_FALSE(source.seekRuntimeTimelineToTimestamp(t0 + 1800000ULL)); } TEST(Source, HostFieldSubmissionStyleContractCreatesEmptyIngress) { SourceTest test; test.style.loadJSON(R"STYLE({ "version": 8, "sources": { "host-submission": {"type": "raster-array", "host-field-submission": true} }, "layers": [] })STYLE"); auto* baseSource = test.style.getSource("host-submission"); ASSERT_NE(baseSource, nullptr); auto* source = baseSource->as(); ASSERT_NE(source, nullptr); EXPECT_TRUE(source->impl().hostFieldSubmission); EXPECT_FALSE(source->impl().field); EXPECT_FALSE(source->impl().timeline); } TEST(Source, HostFieldSubmissionRejectsInvalidWithoutChangingLastValidField) { SourceTest test; style::RasterArraySource source{"host-submission"}; source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); const auto field = fixtureBytes("uniform-east", "field.f32"); const std::vector payload(field.begin(), field.end()); EXPECT_EQ(source.submitHostField(localMetadata(0), payload), style::RasterArrayHostFieldSubmitResult::AcceptedPendingInitialWindow); EXPECT_EQ(source.submitHostField(localMetadata(1), payload), style::RasterArrayHostFieldSubmitResult::InitialWindowReady); ASSERT_TRUE(source.impl().timeline); const auto lastValid = source.impl().timeline->lastEntry().field; auto invalidIdentity = localMetadata(2); const auto fieldID = invalidIdentity.find("production-t2"); ASSERT_NE(fieldID, std::string::npos); invalidIdentity.replace(fieldID, std::string("production-t2").size(), ""); EXPECT_EQ(source.submitHostField(invalidIdentity, payload), style::RasterArrayHostFieldSubmitResult::RejectedAdapter); auto unsupportedLayout = localMetadata(2); const auto layout = unsupportedLayout.find("interleaved-uv"); ASSERT_NE(layout, std::string::npos); unsupportedLayout.replace(layout, std::string("interleaved-uv").size(), "planar-uv"); EXPECT_EQ(source.submitHostField(unsupportedLayout, payload), style::RasterArrayHostFieldSubmitResult::RejectedAdapter); auto unsupportedEncoding = localMetadata(2); const auto encoding = unsupportedEncoding.find("float32-le"); ASSERT_NE(encoding, std::string::npos); unsupportedEncoding.replace(encoding, std::string("float32-le").size(), "float64-le"); EXPECT_EQ(source.submitHostField(unsupportedEncoding, payload), style::RasterArrayHostFieldSubmitResult::RejectedAdapter); auto invalidDimensions = localMetadata(2); const auto width = invalidDimensions.find("\"width\":16"); ASSERT_NE(width, std::string::npos); invalidDimensions.replace(width, std::string("\"width\":16").size(), "\"width\":0"); EXPECT_EQ(source.submitHostField(invalidDimensions, payload), style::RasterArrayHostFieldSubmitResult::RejectedAdapter); auto shortPayload = payload; shortPayload.pop_back(); EXPECT_EQ(source.submitHostField(localMetadata(2), shortPayload), style::RasterArrayHostFieldSubmitResult::RejectedAdapter); auto nonMonotonic = localMetadata(2); const auto validTime = nonMonotonic.find("1704074400000"); ASSERT_NE(validTime, std::string::npos); nonMonotonic.replace(validTime, std::string("1704074400000").size(), "1704069000000"); EXPECT_EQ(source.submitHostField(nonMonotonic, payload), style::RasterArrayHostFieldSubmitResult::RejectedNonMonotonicSnapshot); EXPECT_EQ(source.impl().timeline->size(), 2u); EXPECT_EQ(source.impl().timeline->lastEntry().field, lastValid); EXPECT_EQ(source.impl().timeline->frame().state, style::RasterArrayPlaybackState::Ready); style::RasterArraySource fixtureSource{"fixture", "uniform-east", "metadata", "field"}; EXPECT_EQ(fixtureSource.submitHostField(localMetadata(2), payload), style::RasterArrayHostFieldSubmitResult::RejectedSourceMode); } TEST(Source, RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter) { SourceTest test; const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json"); const auto eastField = fixtureBytes("uniform-east", "field.f32"); const auto northMetadata = fixtureBytes("uniform-north", "metadata.json"); const auto northField = fixtureBytes("uniform-north", "field.f32"); const auto vortexMetadata = fixtureBytes("vortex", "metadata.json"); const auto vortexField = fixtureBytes("vortex", "field.f32"); test.fileSource->sourceResponse = [&](const Resource& resource) -> Response { Response response; const std::string* text = nullptr; const std::string* bytes = nullptr; if (resource.url == "east-metadata") text = &eastMetadata; if (resource.url == "east-field") bytes = &eastField; if (resource.url == "north-metadata") text = &northMetadata; if (resource.url == "north-field") bytes = &northField; if (resource.url == "vortex-metadata") text = &vortexMetadata; if (resource.url == "vortex-field") bytes = &vortexField; if (text) response.data = std::make_unique(*text); if (bytes) response.data = std::make_unique(*bytes); if (!response.data) ADD_FAILURE() << "Unexpected Timeline resource: " << resource.url; return response; }; style::RasterArraySource source{ "source", "uniform-east", "east-metadata", "east-field", "uniform-north", "north-metadata", "north-field", {{"vortex", "vortex-metadata", "vortex-field", 0}}}; source.setObserver(&test.styleObserver); test.styleObserver.sourceChanged = [&](Source&) { if (source.impl().timeline) test.end(); }; source.loadDescription(*test.fileSource); test.run(); ASSERT_TRUE(source.impl().timeline); const auto frame = source.impl().timeline->frame(); ASSERT_TRUE(frame.t0); ASSERT_TRUE(frame.t1); EXPECT_EQ(frame.state, style::RasterArrayPlaybackState::Ready); EXPECT_EQ(frame.t0Index, 0u); EXPECT_TRUE(frame.t0->isValid()); EXPECT_TRUE(frame.t1->isValid()); EXPECT_FALSE(frame.t1->fieldID.empty()); EXPECT_GT(frame.t1->generation, 0u); EXPECT_TRUE(source.impl().timeline->play()); } TEST(Source, RasterArrayGpuReuseConsumesNewRevision) { const auto oldT1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 100); auto wantedNewT0 = oldT1; wantedNewT0.generation = 200; const auto assignedAfterReuse = wantedNewT0; EXPECT_TRUE(particle::sameFieldGpuContent(oldT1, wantedNewT0)); EXPECT_FALSE(particle::fieldIdentityChanged(assignedAfterReuse, wantedNewT0)); } TEST(Source, RasterArrayTimelineKeepsCoreOwnedWindowAndPlaybackControls) { std::vector entries; for (uint64_t index = 0; index < 5; ++index) { entries.push_back({std::make_shared( particle::OfflineFixtureParticleAdapter::load("uniform-east", index + 1)), index * 4000}); } style::RasterArrayTimelineRuntime timeline{std::move(entries)}; EXPECT_EQ(timeline.frame().t0Index, 0u); EXPECT_TRUE(timeline.play()); EXPECT_TRUE(timeline.pause()); EXPECT_TRUE(timeline.setSpeed(0.5f)); EXPECT_FALSE(timeline.setSpeed(3.0f)); EXPECT_TRUE(timeline.step(1)); EXPECT_EQ(timeline.frame().t0Index, 1u); EXPECT_TRUE(timeline.seek(4)); EXPECT_EQ(timeline.frame().state, style::RasterArrayPlaybackState::Ended); } TEST(Source, RasterArrayTimelineCadenceUsesOneSharedDeterministicDeadline) { std::vector entries; for (uint64_t index = 0; index < 3; ++index) { entries.push_back({std::make_shared( particle::OfflineFixtureParticleAdapter::load("uniform-east", index + 1)), index * 4000}); } style::RasterArrayTimelineRuntime timeline{std::move(entries)}; const auto start = std::chrono::steady_clock::time_point{} + std::chrono::seconds(1); EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Fps30)); const auto weatherColorAdvance = timeline.advanceIfDue(start); EXPECT_TRUE(weatherColorAdvance.advanced); const auto particleSameDeadline = timeline.advanceIfDue(start); EXPECT_FALSE(particleSameDeadline.advanced); EXPECT_EQ(particleSameDeadline.frame.cadenceRevision, weatherColorAdvance.frame.cadenceRevision); EXPECT_FALSE(timeline.cadenceDue(start + std::chrono::milliseconds(33))); EXPECT_TRUE(timeline.cadenceDue(start + std::chrono::milliseconds(34))); const auto particleNextDeadline = timeline.advanceIfDue(start + std::chrono::milliseconds(34)); EXPECT_TRUE(particleNextDeadline.advanced); const auto weatherColorLateSameDeadline = timeline.advanceIfDue(start + std::chrono::milliseconds(34)); EXPECT_FALSE(weatherColorLateSameDeadline.advanced); EXPECT_TRUE(timeline.pause()); const auto pausedColorFrame = timeline.frame(); EXPECT_EQ(pausedColorFrame.state, style::RasterArrayPlaybackState::Paused); // A paused WeatherColor-only layer asks no continuous repaint; if another // map frame arrives, one shared advance is harmless and does not move MIX. const auto pausedAdvance = timeline.advanceIfDue(start + std::chrono::milliseconds(68)); EXPECT_TRUE(pausedAdvance.advanced); EXPECT_FLOAT_EQ(pausedAdvance.frame.mix, pausedColorFrame.mix); EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Fps60)); const auto afterSwitch = timeline.advanceIfDue(start + std::chrono::milliseconds(68)); EXPECT_TRUE(afterSwitch.advanced); EXPECT_FALSE(timeline.advanceIfDue(start + std::chrono::milliseconds(84)).advanced); EXPECT_TRUE(timeline.advanceIfDue(start + std::chrono::milliseconds(85)).advanced); const auto missedDeadline = timeline.advanceIfDue(start + std::chrono::seconds(5)); EXPECT_TRUE(missedDeadline.advanced); EXPECT_FALSE(timeline.advanceIfDue(start + std::chrono::seconds(5)).advanced); EXPECT_TRUE(timeline.setTargetFps(style::RasterArrayTargetFps::Auto)); EXPECT_TRUE(timeline.cadenceDue(start)); } TEST(Source, RasterArrayRuntimeFieldWindowAdvancesWithoutReplacingTheSource) { SourceTest test; const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json"); const auto eastField = fixtureBytes("uniform-east", "field.f32"); const auto northMetadata = fixtureBytes("uniform-north", "metadata.json"); const auto northField = fixtureBytes("uniform-north", "field.f32"); test.fileSource->sourceResponse = [&](const Resource& resource) -> Response { Response response; if (resource.url == "east-metadata") { response.data = std::make_unique(eastMetadata); } else if (resource.url == "east-field") { response.data = std::make_unique(eastField); } else if (resource.url == "north-metadata") { response.data = std::make_unique(northMetadata); } else if (resource.url == "north-field") { response.data = std::make_unique(northField); } else { ADD_FAILURE() << "Unexpected RasterArray resource: " << resource.url; } return response; }; style::RasterArraySource source{ "source", "uniform-east", "east-metadata", "east-field", "uniform-north", "north-metadata", "north-field"}; source.setObserver(&test.styleObserver); bool initialLoad = true; uint32_t sourceChangedCount = 0; test.styleObserver.sourceChanged = [&](Source&) { ++sourceChangedCount; if (initialLoad) { initialLoad = false; test.end(); } }; source.loadDescription(*test.fileSource); test.run(); ASSERT_TRUE(source.impl().field); ASSERT_TRUE(source.impl().nextField); const auto initialT0Generation = source.impl().field->generation; const auto initialT1Generation = source.impl().nextField->generation; const auto vortex = particle::OfflineFixtureParticleAdapter::load("vortex", initialT1Generation + 1); const auto nodata = particle::OfflineFixtureParticleAdapter::load("nodata", vortex.generation + 1); EXPECT_EQ(source.updateRuntimeFieldWindow(vortex), style::RasterArrayRuntimeFieldUpdateResult::Applied); EXPECT_EQ(source.impl().field->fieldID, "uniform-north"); EXPECT_EQ(source.impl().field->generation, initialT1Generation); EXPECT_EQ(source.impl().nextField->fieldID, "vortex"); EXPECT_EQ(source.impl().nextField->generation, vortex.generation); EXPECT_EQ(sourceChangedCount, 2u); EXPECT_EQ(source.updateRuntimeFieldWindow(vortex), style::RasterArrayRuntimeFieldUpdateResult::Noop); auto invalid = nodata; invalid.velocity.clear(); EXPECT_EQ(source.updateRuntimeFieldWindow(invalid), style::RasterArrayRuntimeFieldUpdateResult::RejectedInvalidSnapshot); EXPECT_EQ(source.impl().field->fieldID, "uniform-north"); EXPECT_EQ(source.impl().nextField->fieldID, "vortex"); EXPECT_EQ(source.updateRuntimeFieldWindow(nodata), style::RasterArrayRuntimeFieldUpdateResult::Applied); EXPECT_EQ(source.impl().field->fieldID, "vortex"); EXPECT_EQ(source.impl().nextField->fieldID, "nodata"); EXPECT_EQ(sourceChangedCount, 3u); EXPECT_NE(source.impl().field->generation, initialT0Generation); EXPECT_EQ(source.repeatRuntimeFieldWindow(), style::RasterArrayRuntimeFieldUpdateResult::Noop); EXPECT_EQ(source.impl().field->fieldID, "vortex"); EXPECT_EQ(source.impl().nextField->fieldID, "nodata"); EXPECT_EQ(sourceChangedCount, 3u); } TEST(Source, RasterArrayTimeWindowKeepsIndependentCompatibleSnapshots) { const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 11); const auto north = particle::OfflineFixtureParticleAdapter::load("uniform-north", 12); const particle::ParticleFieldTimeWindow window{east, north}; EXPECT_TRUE(window.isValid()); EXPECT_NE(window.t0.generation, window.t1.generation); EXPECT_TRUE(window.t0.isTimeInterpolationCompatibleWith(window.t1)); EXPECT_TRUE(particle::fieldIdentityChanged(window.t0, window.t1)); } TEST(Source, RasterArrayTimeWindowRejectsIncompatibleGrid) { const auto east = particle::OfflineFixtureParticleAdapter::load("uniform-east", 13); auto incompatible = east; ++incompatible.width; const particle::ParticleFieldTimeWindow window{east, incompatible}; EXPECT_FALSE(east.isTimeInterpolationCompatibleWith(incompatible)); EXPECT_FALSE(window.isValid()); } TEST(Source, LoadingCorrupt) { SourceTest test; test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("url", resource.url); Response response; response.data = std::make_unique("CORRUPTED"); return response; }; test.styleObserver.sourceError = [&](Source& source, std::exception_ptr error) { EXPECT_EQ("source", source.getID()); EXPECT_EQ("Invalid value. at offset 0", util::toString(error)); test.end(); }; VectorSource source("source", "url"); source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); test.run(); } TEST(Source, RasterTileEmpty) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.noContent = true; return response; }; RasterLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileChanged = [&](RenderSource& source_, const OverscaledTileID&) { EXPECT_EQ("source", source_.baseImpl->id); test.end(); }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterDEMTileEmpty) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.noContent = true; return response; }; HillshadeLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterDEMSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileChanged = [&](RenderSource& source_, const OverscaledTileID&) { EXPECT_EQ("source", source_.baseImpl->id); test.end(); }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, VectorTileEmpty) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.noContent = true; return response; }; LineLayer layer("id", "source"); layer.setSourceLayer("water"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; VectorSource source("source", tileset); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileChanged = [&](RenderSource& source_, const OverscaledTileID&) { EXPECT_EQ("source", source_.baseImpl->id); test.end(); }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterTileFail) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.error = std::make_unique(Response::Error::Reason::Other, "Failed by the test case"); return response; }; RasterLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileError = [&](RenderSource& source_, const OverscaledTileID& tileID, std::exception_ptr error) { EXPECT_EQ(SourceType::Raster, source_.baseImpl->type); EXPECT_EQ(OverscaledTileID(0, 0, 0), tileID); EXPECT_EQ("Failed by the test case", util::toString(error)); test.end(); }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterDEMTileFail) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.error = std::make_unique(Response::Error::Reason::Other, "Failed by the test case"); return response; }; HillshadeLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterDEMSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileError = [&](RenderSource& source_, const OverscaledTileID& tileID, std::exception_ptr error) { EXPECT_EQ(SourceType::RasterDEM, source_.baseImpl->type); EXPECT_EQ(OverscaledTileID(0, 0, 0), tileID); EXPECT_EQ("Failed by the test case", util::toString(error)); test.end(); }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, VectorTileFail) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.error = std::make_unique(Response::Error::Reason::Other, "Failed by the test case"); return response; }; LineLayer layer("id", "source"); layer.setSourceLayer("water"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; VectorSource source("source", tileset); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileError = [&](RenderSource& source_, const OverscaledTileID& tileID, std::exception_ptr error) { EXPECT_EQ(SourceType::Vector, source_.baseImpl->type); EXPECT_EQ(OverscaledTileID(0, 0, 0), tileID); EXPECT_EQ("Failed by the test case", util::toString(error)); test.end(); }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterTileCorrupt) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.data = std::make_unique("CORRUPTED"); return response; }; RasterLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileError = [&](RenderSource& source_, const OverscaledTileID& tileID, std::exception_ptr error) { EXPECT_EQ(source_.baseImpl->type, SourceType::Raster); EXPECT_EQ(OverscaledTileID(0, 0, 0), tileID); EXPECT_TRUE(bool(error)); // Not asserting on platform-specific error text. test.end(); }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterDEMTileCorrupt) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.data = std::make_unique("CORRUPTED"); return response; }; HillshadeLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; ; Tileset tileset; tileset.tiles = {"tiles"}; RasterDEMSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileError = [&](RenderSource& source_, const OverscaledTileID& tileID, std::exception_ptr error) { EXPECT_EQ(source_.baseImpl->type, SourceType::RasterDEM); EXPECT_EQ(OverscaledTileID(0, 0, 0), tileID); EXPECT_TRUE(bool(error)); // Not asserting on platform-specific error text. test.end(); }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, VectorTileCorrupt) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { Response response; response.data = std::make_unique("CORRUPTED"); return response; }; LineLayer layer("id", "source"); layer.setSourceLayer("water"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; VectorSource source("source", tileset); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileError = [&](RenderSource& source_, const OverscaledTileID& tileID, std::exception_ptr error) { EXPECT_EQ(source_.baseImpl->type, SourceType::Vector); EXPECT_EQ(OverscaledTileID(0, 0, 0), tileID); EXPECT_EQ(util::toString(error), "unknown pbf field type exception"); test.end(); }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterTileCancel) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { test.end(); return std::optional(); }; RasterLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileChanged = [&](RenderSource&, const OverscaledTileID&) { FAIL() << "Should never be called"; }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterDEMTileCancel) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { test.end(); return std::optional(); }; HillshadeLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterDEMSource source("source", tileset, 512); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileChanged = [&](RenderSource&, const OverscaledTileID&) { FAIL() << "Should never be called"; }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, VectorTileCancel) { SourceTest test; test.fileSource->tileResponse = [&](const Resource&) { test.end(); return std::optional(); }; LineLayer layer("id", "source"); layer.setSourceLayer("water"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; VectorSource source("source", tileset); source.loadDescription(*test.fileSource); test.renderSourceObserver.tileChanged = [&](RenderSource&, const OverscaledTileID&) { FAIL() << "Should never be called"; }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterTileAttribution) { SourceTest test; RasterLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; std::string mapboxOSM = ("© " "Mapbox " "©️ " "OpenStreetMap"); test.fileSource->tileResponse = [&](const Resource&) { Response response; response.noContent = true; return response; }; test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("url", resource.url); Response response; response.data = std::make_unique(R"TILEJSON({ "tilejson": "2.1.0", "attribution": ")TILEJSON" + mapboxOSM + R"TILEJSON(", "tiles": [ "tiles" ] })TILEJSON"); return response; }; test.styleObserver.sourceChanged = [&](Source& source) { EXPECT_EQ(mapboxOSM, source.getAttribution()); EXPECT_FALSE(mapboxOSM.find("©️ OpenStreetMap") == std::string::npos); test.end(); }; RasterSource source("source", "url", 512); source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, RasterDEMTileAttribution) { SourceTest test; HillshadeLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; std::string mapbox = ("© " "Mapbox "); test.fileSource->tileResponse = [&](const Resource&) { Response response; response.noContent = true; return response; }; test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("url", resource.url); Response response; response.data = std::make_unique(R"TILEJSON({ "tilejson": "2.1.0", "attribution": ")TILEJSON" + mapbox + R"TILEJSON(", "tiles": [ "tiles" ] })TILEJSON"); return response; }; test.styleObserver.sourceChanged = [&](Source& source) { EXPECT_EQ(mapbox, source.getAttribution()); test.end(); }; RasterDEMSource source("source", "url", 512); source.setObserver(&test.styleObserver); source.loadDescription(*test.fileSource); auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.run(); } TEST(Source, GeoJSonSourceUrlUpdate) { SourceTest test; test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("http://source-url.ext", resource.url); Response response; response.data = std::make_unique( R"({"geometry": {"type": "Point", "coordinates": [1.1, 1.1]}, "type": "Feature", "properties": {}})"); return response; }; test.styleObserver.sourceLoaded = [&](Source&) { // Should be called (test will hang if it doesn't) test.end(); }; GeoJSONSource source("source"); source.setObserver(&test.styleObserver); // Load initial, so the source state will be loaded=true source.loadDescription(*test.fileSource); // Schedule an update test.loop.invoke([&]() { // Update the url source.setURL(std::string("http://source-url.ext")); source.loadDescription(*test.fileSource); }); test.run(); } TEST(Source, ImageSourceImageUpdate) { SourceTest test; test.fileSource->response = [&](const Resource& resource) { EXPECT_EQ("http://url", resource.url); Response response; response.data = std::make_unique(util::read_file("test/fixtures/image/no_profile.png")); return response; }; test.styleObserver.sourceChanged = [&](Source&) { // Should be called (test will hang if it doesn't) test.end(); }; std::array coords; ImageSource source("source", coords); source.setURL("http://url"); source.setObserver(&test.styleObserver); // Load initial, so the source state will be loaded=true source.loadDescription(*test.fileSource); PremultipliedImage rgba({1, 1}); rgba.data[0] = 255; rgba.data[1] = 254; rgba.data[2] = 253; rgba.data[3] = 0; // Schedule an update test.loop.invoke([&]() { // Update the url source.setImage(std::move(rgba)); }); test.run(); } TEST(Source, CustomGeometrySourceSetTileData) { SourceTest test; CustomGeometrySource source("source", CustomGeometrySource::Options()); source.loadDescription(*test.fileSource); LineLayer layer("id", "source"); layer.setSourceLayer("water"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; test.renderSourceObserver.tileChanged = [&](RenderSource& source_, const OverscaledTileID&) { EXPECT_EQ("source", source_.baseImpl->id); test.end(); }; test.renderSourceObserver.tileError = [&](RenderSource&, const OverscaledTileID&, std::exception_ptr) { FAIL() << "Should never be called"; }; auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.loop.invoke([&]() { // Set Tile Data source.setTileData(CanonicalTileID(0, 0, 0), GeoJSON{FeatureCollection{}}); }); test.run(); } namespace { class FakeTileSource; class FakeTile : public Tile { public: FakeTile(FakeTileSource& source_, const OverscaledTileID& tileID, TileObserver* observer_) : Tile(Tile::Kind::Geometry, tileID, "FakeTileSource", observer_), source(source_) { renderable = true; } void setNecessity(TileNecessity necessity) override; void setUpdateParameters(const TileUpdateParameters&) override; bool layerPropertiesUpdated(const Immutable&) override { return true; } std::unique_ptr createRenderData() override { return nullptr; } void cancel() override {} private: FakeTileSource& source; }; class FakeTileSource : public RenderTileSetSource { public: MOCK_METHOD1(tileSetNecessity, void(TileNecessity)); MOCK_METHOD1(tileSetMinimumUpdateInterval, void(Duration)); explicit FakeTileSource(Immutable impl_, const TaggedScheduler& threadPool_) : RenderTileSetSource(std::move(impl_), threadPool_) {} void updateInternal(const Tileset& tileset, const std::vector>& layers, const bool needsRendering, const bool needsRelayout, const TileParameters& parameters) override { tilePyramid.update(layers, needsRendering, needsRelayout, parameters, *baseImpl, util::tileSize_I, tileset.zoomRange, tileset.bounds, [&](const OverscaledTileID& tileID, TileObserver* observer_) { return std::make_unique(*this, tileID, observer_); }); } const std::optional& getTileset() const override { return static_cast(*baseImpl).tileset; } }; void FakeTile::setNecessity(TileNecessity necessity) { source.tileSetNecessity(necessity); } void FakeTile::setUpdateParameters(const TileUpdateParameters& params) { source.tileSetMinimumUpdateInterval(params.minimumUpdateInterval); } } // namespace TEST(Source, InvisibleSourcesTileNecessity) { SourceTest test; VectorSource initialized("source", Tileset{{"tiles"}}); initialized.loadDescription(*test.fileSource); FakeTileSource renderTilesetSource{initialized.baseImpl, test.threadPool}; RenderSource* renderSource = &renderTilesetSource; LineLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; EXPECT_CALL(renderTilesetSource, tileSetMinimumUpdateInterval).Times(testing::AnyNumber()); EXPECT_CALL(renderTilesetSource, tileSetNecessity(TileNecessity::Required)).Times(1); renderSource->update(initialized.baseImpl, layers, true, true, test.tileParameters()); // Necessity for invisible tiles must be set to `optional`. EXPECT_CALL(renderTilesetSource, tileSetNecessity(TileNecessity::Optional)).Times(1); renderSource->update(initialized.baseImpl, layers, false, false, test.tileParameters()); // Necessity is again `required` once tiles get back visible. EXPECT_CALL(renderTilesetSource, tileSetNecessity(TileNecessity::Required)).Times(1); renderSource->update(initialized.baseImpl, layers, true, false, test.tileParameters()); } TEST(Source, SourceMinimumUpdateInterval) { SourceTest test; VectorSource initialized("source", Tileset{{"tiles"}}); initialized.loadDescription(*test.fileSource); FakeTileSource renderTilesetSource{initialized.baseImpl, test.threadPool}; RenderSource* renderSource = &renderTilesetSource; LineLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Duration minimumTileUpdateInterval = initialized.getMinimumTileUpdateInterval(); auto baseImpl = initialized.baseImpl; EXPECT_EQ(Duration::zero(), minimumTileUpdateInterval); EXPECT_CALL(renderTilesetSource, tileSetNecessity).Times(testing::AnyNumber()); EXPECT_CALL(renderTilesetSource, tileSetMinimumUpdateInterval(minimumTileUpdateInterval)).Times(1); renderSource->update(baseImpl, layers, true, false, test.tileParameters()); initialized.setMinimumTileUpdateInterval(Seconds(1)); EXPECT_NE(baseImpl, initialized.baseImpl) << "Source impl was updated"; baseImpl = initialized.baseImpl; initialized.setMinimumTileUpdateInterval(Seconds(1)); // Set the same interval again. EXPECT_EQ(baseImpl, initialized.baseImpl) << "Source impl was not updated"; minimumTileUpdateInterval = initialized.getMinimumTileUpdateInterval(); EXPECT_EQ(Seconds(1), minimumTileUpdateInterval); EXPECT_CALL(renderTilesetSource, tileSetMinimumUpdateInterval(minimumTileUpdateInterval)).Times(1); renderSource->update(baseImpl, layers, true, false, test.tileParameters()); initialized.setMinimumTileUpdateInterval(Seconds(2)); minimumTileUpdateInterval = initialized.getMinimumTileUpdateInterval(); EXPECT_EQ(Seconds(2), minimumTileUpdateInterval); // No network activity for invisible tiles, and no reason to set the update interval. EXPECT_CALL(renderTilesetSource, tileSetMinimumUpdateInterval(minimumTileUpdateInterval)).Times(0); renderSource->update(initialized.baseImpl, layers, false, false, test.tileParameters()); // Tiles got visible, set the update interval now. EXPECT_CALL(renderTilesetSource, tileSetMinimumUpdateInterval(minimumTileUpdateInterval)).Times(1); renderSource->update(initialized.baseImpl, layers, true, false, test.tileParameters()); } TEST(Source, RenderTileSetSourceUpdate) { SourceTest test; class FakeRenderTileSetSource : public RenderTileSetSource { public: explicit FakeRenderTileSetSource(Immutable impl_, TaggedScheduler threadPool_) : RenderTileSetSource(std::move(impl_), threadPool_) {} MOCK_METHOD0(mockedUpdateInternal, void()); void updateInternal(const Tileset&, const std::vector>&, const bool, const bool, const TileParameters&) override { mockedUpdateInternal(); } const std::optional& getTileset() const override { return static_cast(*baseImpl).tileset; } }; VectorSource initialized("source", Tileset{{"tiles"}}); initialized.loadDescription(*test.fileSource); FakeRenderTileSetSource renderTilesetSource{initialized.baseImpl, test.threadPool}; LineLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; // Check that `updateInternal()` is called even if the updated source has not yet loaded description. EXPECT_CALL(renderTilesetSource, mockedUpdateInternal()).Times(2); RenderSource* renderSource = &renderTilesetSource; renderSource->update(initialized.baseImpl, layers, true, true, test.tileParameters()); VectorSource uninitialized("source", "http://url"); renderSource->update(uninitialized.baseImpl, layers, true, true, test.tileParameters()); } TEST(Source, VectorSourceSetTiles) { SourceTest test; test.styleObserver.sourceChanged = [&](Source& source) { EXPECT_EQ(source.as()->getTiles(), std::vector{"new"}); test.end(); }; VectorSource source("source", Tileset{{"url"}}); source.setObserver(&test.styleObserver); source.setTiles({"unused"}); // make sure early setTiles does not segfault source.loadDescription(*test.fileSource); EXPECT_EQ(source.as()->getTiles(), std::vector{"url"}); source.setTiles({"new"}); test.run(); } TEST(Source, VectorSourceUrlSetTiles) { SourceTest test; test.styleObserver.sourceLoaded = [&](Source& source) { EXPECT_EQ(source.as()->getTiles(), std::vector{"tiles"}); source.as()->setTiles({"new"}); }; int count = 0; test.styleObserver.sourceChanged = [&](Source& source) { if (count++) { EXPECT_EQ(source.as()->getTiles(), std::vector{"new"}); test.end(); } }; test.fileSource->sourceResponse = [&](const Resource& resource) { EXPECT_EQ("url", resource.url); Response response; response.data = std::make_unique(R"({"tiles": ["tiles"]})"); return response; }; VectorSource source("source", "url"); source.setObserver(&test.styleObserver); source.setTiles({"unused"}); source.loadDescription(*test.fileSource); test.run(); } TEST(Source, GeoJSONSourceTilesAfterDataReset) { SourceTest test; GeoJSONSource source("source"); auto geoJSONData = GeoJSONData::create( mapbox::geojson::parse( R"({"geometry": {"type": "Point", "coordinates": [1.1, 1.1]}, "type": "Feature", "properties": {}})"), Scheduler::GetSequenced()); source.setGeoJSONData(geoJSONData); RenderGeoJSONSource renderSource{staticImmutableCast(source.baseImpl), test.threadPool}; CircleLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; static_cast(renderSource).update(source.baseImpl, layers, true, true, test.tileParameters()); EXPECT_FALSE(renderSource.isLoaded()); // loaded == false, means that the // source contains pending tiles. source.setGeoJSONData(nullptr); static_cast(renderSource).update(source.baseImpl, layers, true, true, test.tileParameters()); EXPECT_FALSE(renderSource.isLoaded()); // Tiles remain in continuous mode. source.setGeoJSONData(geoJSONData); static_cast(renderSource).update(source.baseImpl, layers, true, true, test.tileParameters()); EXPECT_FALSE(renderSource.isLoaded()); source.setGeoJSONData(nullptr); static_cast(renderSource) .update(source.baseImpl, layers, true, true, test.tileParameters(MapMode::Static)); EXPECT_TRUE(renderSource.isLoaded()); // Tiles are reset in static mode. } TEST(Source, SetMaxParentOverscaleFactor) { SourceTest test; test.transform.jumpTo(CameraOptions().withCenter(LatLng()).withZoom(8.0)); test.transformState = test.transform.getState(); util::Timer timer; FixtureLog log; test.fileSource->tileResponse = [&](const Resource& res) { if (res.tileData->z == 5) { timer.start(Milliseconds(10), Duration::zero(), [&] { test.end(); }); } // No tiles above zoom level 5 should be requested. EXPECT_LE(5, int(res.tileData->z)); Response response; response.noContent = true; return response; }; RasterLayer layer("id", "source"); Immutable layerProperties = makeMutable( staticImmutableCast(layer.baseImpl)); std::vector> layers{layerProperties}; Tileset tileset; tileset.tiles = {"tiles"}; RasterSource source("source", tileset, 512); ASSERT_EQ(std::nullopt, source.getMaxOverscaleFactorForParentTiles()); source.setMaxOverscaleFactorForParentTiles(3); ASSERT_EQ(3, *source.getMaxOverscaleFactorForParentTiles()); source.loadDescription(*test.fileSource); auto renderSource = RenderSource::create(source.baseImpl, test.threadPool); renderSource->setObserver(&test.renderSourceObserver); renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); test.renderSourceObserver.tileChanged = [&](RenderSource&, const OverscaledTileID&) { renderSource->update(source.baseImpl, layers, true, true, test.tileParameters()); }; test.run(); EXPECT_EQ( 1u, log.count( {EventSeverity::Warning, Event::Style, -1, "Parent tile overscale factor will cap prefetch delta to 3"})); EXPECT_EQ(0u, log.uncheckedCount()); }