diff --git a/.forgejo/workflows/map-generator.yml b/.forgejo/workflows/map-generator.yml index 51ca964f0..6f97ab2d6 100644 --- a/.forgejo/workflows/map-generator.yml +++ b/.forgejo/workflows/map-generator.yml @@ -17,6 +17,11 @@ on: required: false default: false type: boolean + run-panoramax: + description: 'Update Panoramax imagery?' + required: false + default: false + type: boolean run-tiger: description: 'Update TIGER address data?' required: false @@ -209,6 +214,69 @@ jobs: --data-urlencode topic=codeberg-bot \ --data-urlencode 'content=Isolines are done!' + update-panoramax: + if: inputs.run-panoramax + name: Update Panoramax + runs-on: mapfilemaker + needs: + - clone-repos + container: + image: codeberg.org/comaps/maps_generator:f6d53d54f794 + volumes: + - /mnt/4tbexternal/:/mnt/4tbexternal/ + - /mnt/4tbexternal/osm-planet:/home/planet + concurrency: + group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + steps: + - uses: actions/cache@v4 + with: + path: "~" + key: cache-${{ github.run_id }}-${{ github.run_attempt }} + - name: Install Python dependencies + shell: bash + run: | + pip install --upgrade pip + pip install pyarrow duckdb + - name: Download Panoramax Geoparquet + shell: bash + run: | + mkdir -p /home/planet/panoramax + cd /home/planet/panoramax + # Download the global Panoramax geoparquet file (20GB) + if [ ! -f panoramax.parquet ]; then + echo "Downloading Panoramax geoparquet..." + curl -L -o panoramax.parquet https://api.panoramax.xyz/data/geoparquet/panoramax.parquet + else + echo "panoramax.parquet already exists, skipping download" + fi + - name: Process Panoramax to per-country files + shell: bash + run: | + cd ~/comaps + mkdir -p /home/planet/panoramax/countries + python3 tools/python/maps_generator/panoramax_preprocessor.py \ + --input /home/planet/panoramax/panoramax.parquet \ + --output /home/planet/panoramax/countries \ + --polygons ~/comaps/data/packed_polygons.bin + - name: Check panoramax files + shell: bash + run: | + NUMPANO=$(ls -1 /home/planet/panoramax/countries/*.panoramax 2>/dev/null | wc -l) + echo "Found $NUMPANO panoramax country files" + if [ $NUMPANO -lt 5 ]; then + echo "ERROR: Did generation fail? Expected at least 5 country files" + exit 1 + fi + - name: Notify Zulip + run: | + curl -X POST https://comaps.zulipchat.com/api/v1/messages \ + -u $ZULIP_BOT_EMAIL:$ZULIP_API_KEY \ + --data-urlencode type=stream \ + --data-urlencode 'to="DevOps"' \ + --data-urlencode topic=codeberg-bot \ + --data-urlencode 'content=Panoramax processing is done!' + update-tiger: if: inputs.run-tiger name: Update TIGER diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtonFactory.java b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtonFactory.java index 8d4898b3c..99304d7bc 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtonFactory.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtonFactory.java @@ -74,6 +74,11 @@ public class PlacePageButtonFactory titleId = R.string.avoid_ferry; yield R.drawable.ic_avoid_ferry; } + case PANORAMAX -> + { + titleId = R.string.panoramax; + yield R.drawable.ic_camera; + } case MORE -> { titleId = R.string.placepage_more_button; diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java index 5664f5933..6652afb45 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageButtons.java @@ -144,6 +144,7 @@ public final class PlacePageButtons extends Fragment implements Observer onAvoidTollBtnClicked(); case ROUTE_AVOID_UNPAVED -> onAvoidUnpavedBtnClicked(); case ROUTE_AVOID_FERRY -> onAvoidFerryBtnClicked(); + case PANORAMAX -> onPanoramaxBtnClicked(); } } @@ -499,6 +500,19 @@ public class PlacePageController requireActivity().finish(); } + private void onPanoramaxBtnClicked() + { + if (mMapObject == null) + return; + String url = Framework.nativeGetPanoramaxUrl(); + if (!TextUtils.isEmpty(url)) + { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(android.net.Uri.parse(url)); + startActivity(intent); + } + } + private void onRouteFromBtnClicked() { if (mMapObject == null) @@ -637,6 +651,10 @@ public class PlacePageController buttons.add(mapObject.isBookmark() ? PlacePageButtons.ButtonType.BOOKMARK_DELETE : PlacePageButtons.ButtonType.BOOKMARK_SAVE); } + + // Add Panoramax button if imagery is available + if (Framework.nativeHasPanoramax()) + buttons.add(PlacePageButtons.ButtonType.PANORAMAX); } mViewModel.setCurrentButtons(buttons); } diff --git a/android/sdk/src/main/cpp/app/organicmaps/sdk/Framework.cpp b/android/sdk/src/main/cpp/app/organicmaps/sdk/Framework.cpp index 29ee2cb8e..b22d2aa9f 100644 --- a/android/sdk/src/main/cpp/app/organicmaps/sdk/Framework.cpp +++ b/android/sdk/src/main/cpp/app/organicmaps/sdk/Framework.cpp @@ -1764,6 +1764,16 @@ JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_Framework_nativeHasPlacePage return static_cast(frm()->HasPlacePageInfo()); } +JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_Framework_nativeHasPanoramax(JNIEnv *, jclass) +{ + return static_cast(g_framework->GetPlacePageInfo().HasPanoramax()); +} + +JNIEXPORT jstring JNICALL Java_app_organicmaps_sdk_Framework_nativeGetPanoramaxUrl(JNIEnv * env, jclass) +{ + return jni::ToJavaString(env, g_framework->GetPlacePageInfo().GetPanoramaxUrl()); +} + JNIEXPORT void JNICALL Java_app_organicmaps_sdk_Framework_nativeMemoryWarning(JNIEnv *, jclass) { return frm()->MemoryWarning(); diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/Framework.java b/android/sdk/src/main/java/app/organicmaps/sdk/Framework.java index c6bb4a1b2..7fbc64a8c 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/Framework.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/Framework.java @@ -349,6 +349,8 @@ public class Framework * @return true if c++ framework has initialized internal place page object, otherwise - false. */ public static native boolean nativeHasPlacePageInfo(); + public static native boolean nativeHasPanoramax(); + public static native String nativeGetPanoramaxUrl(); public static native void nativeMemoryWarning(); public static native void nativeSaveRoute(); diff --git a/data/mapcss-mapping.csv b/data/mapcss-mapping.csv index 575bcaf20..35d121ef1 100644 --- a/data/mapcss-mapping.csv +++ b/data/mapcss-mapping.csv @@ -1758,3 +1758,4 @@ amenity|luggage_locker;1629; building|guardhouse;[building=guardhouse],[amenity=security_booth],[amenity=checkpoint];;;;1630; office|security;1631; shop|lighting;1632; +panoramax|image;1633; diff --git a/generator/final_processor_country.cpp b/generator/final_processor_country.cpp index e35960c12..020fb4cff 100644 --- a/generator/final_processor_country.cpp +++ b/generator/final_processor_country.cpp @@ -7,6 +7,7 @@ #include "generator/feature_builder.hpp" #include "generator/final_processor_utils.hpp" #include "generator/isolines_generator.hpp" +#include "generator/panoramax_generator.hpp" #include "generator/mini_roundabout_transformer.hpp" #include "generator/node_mixer.hpp" #include "generator/osm2type.hpp" @@ -68,6 +69,10 @@ void CountryFinalProcessor::Process() if (!m_isolinesPath.empty()) AddIsolines(); + LOG(LINFO, ("Adding panoramax...")); + if (!m_panoramaxPath.empty()) + AddPanoramax(); + // DropProhibitedSpeedCameras(); LOG(LINFO, ("Processing building parts...")); ProcessBuildingParts(); @@ -293,6 +298,22 @@ void CountryFinalProcessor::AddAddresses() LOG(LINFO, ("Total addresses:", totalStats)); } +void CountryFinalProcessor::AddPanoramax() +{ + if (m_panoramaxPath.empty()) + return; + + PanoramaxFeaturesGenerator panoramaxGenerator(m_panoramaxPath); + ForEachMwmTmp(m_temporaryMwmPath, [&](auto const & name, auto const & path) + { + if (!IsCountry(name)) + return; + + FeatureBuilderWriter writer(path, FileWriter::Op::OP_APPEND); + panoramaxGenerator.GeneratePanoramax(name, [&](auto const & fb) { writer.Write(fb); }); + }, m_threadsCount); +} + void CountryFinalProcessor::ProcessCoastline() { /// @todo We can remove MinSize at all. diff --git a/generator/final_processor_country.hpp b/generator/final_processor_country.hpp index 7f6b270ea..dc5af0c91 100644 --- a/generator/final_processor_country.hpp +++ b/generator/final_processor_country.hpp @@ -24,6 +24,7 @@ public: void SetIsolinesDir(std::string const & dir) { m_isolinesPath = dir; } void SetAddressesDir(std::string const & dir) { m_addressPath = dir; } + void SetPanoramaxDir(std::string const & dir) { m_panoramaxPath = dir; } void SetCityBoundariesFiles(std::string const & collectorFile) { m_boundariesCollectorFile = collectorFile; } @@ -39,6 +40,7 @@ private: void AddFakeNodes(); void AddIsolines(); void AddAddresses(); + void AddPanoramax(); void DropProhibitedSpeedCameras(); // void Finish(); @@ -47,7 +49,7 @@ private: std::string m_borderPath; std::string m_temporaryMwmPath; std::string m_intermediateDir; - std::string m_isolinesPath, m_addressPath; + std::string m_isolinesPath, m_addressPath, m_panoramaxPath; std::string m_boundariesCollectorFile; std::string m_coastlineGeomFilename; std::string m_worldCoastsFilename; diff --git a/generator/generate_info.hpp b/generator/generate_info.hpp index 8400352d8..65dfe2cb7 100644 --- a/generator/generate_info.hpp +++ b/generator/generate_info.hpp @@ -39,8 +39,8 @@ struct GenerateInfo std::string m_cacheDir; - // External folders with additional preprocessed data (isolines, addresses). - std::string m_isolinesDir, m_addressesDir; + // External folders with additional preprocessed data (isolines, addresses, panoramax). + std::string m_isolinesDir, m_addressesDir, m_panoramaxDir; // Current generated file name if --output option is defined. std::string m_fileName; diff --git a/generator/generator_tool/generator_tool.cpp b/generator/generator_tool/generator_tool.cpp index c0dcc8cef..f06f8721d 100644 --- a/generator/generator_tool/generator_tool.cpp +++ b/generator/generator_tool/generator_tool.cpp @@ -107,6 +107,7 @@ DEFINE_string(nodes_list_path, "", DEFINE_bool(generate_isolines_info, false, "Generate the isolines info section"); DEFINE_string(isolines_path, "", "Path to isolines directory. If set, adds isolines linear features."); DEFINE_string(addresses_path, "", "Path to addresses directory. If set, adds addr:interpolation features."); +DEFINE_string(panoramax_path, "", "Path to panoramax directory. If set, adds panoramax imagery point features."); // Routing. DEFINE_bool(make_routing_index, false, "Make sections with the routing information."); @@ -243,6 +244,7 @@ MAIN_WITH_ERROR_HANDLING([](int argc, char ** argv) genInfo.m_complexHierarchyFilename = FLAGS_complex_hierarchy_data; genInfo.m_isolinesDir = FLAGS_isolines_path; genInfo.m_addressesDir = FLAGS_addresses_path; + genInfo.m_panoramaxDir = FLAGS_panoramax_path; // Use merged style. GetStyleReader().SetCurrentStyle(MapStyleMerged); diff --git a/generator/raw_generator.cpp b/generator/raw_generator.cpp index cf90df8dd..e0fdb13bd 100644 --- a/generator/raw_generator.cpp +++ b/generator/raw_generator.cpp @@ -182,6 +182,7 @@ RawGenerator::FinalProcessorPtr RawGenerator::CreateCountryFinalProcessor(Affili auto finalProcessor = std::make_shared(affiliations, m_genInfo.m_tmpDir, m_threadsCount); finalProcessor->SetIsolinesDir(m_genInfo.m_isolinesDir); finalProcessor->SetAddressesDir(m_genInfo.m_addressesDir); + finalProcessor->SetPanoramaxDir(m_genInfo.m_panoramaxDir); finalProcessor->SetMiniRoundabouts(m_genInfo.GetIntermediateFileName(MINI_ROUNDABOUTS_FILENAME)); finalProcessor->SetAddrInterpolation(m_genInfo.GetIntermediateFileName(ADDR_INTERPOL_FILENAME)); if (addAds) diff --git a/libs/map/framework.cpp b/libs/map/framework.cpp index ba683261f..dee9a3b0b 100644 --- a/libs/map/framework.cpp +++ b/libs/map/framework.cpp @@ -706,6 +706,7 @@ void Framework::FillInfoFromFeatureType(FeatureType & ft, place_page::Info & inf info.SetFromFeatureType(ft); FillDescription(ft, info); + CheckPanoramaxImagery(info); auto const mwmInfo = ft.GetID().m_mwmId.GetInfo(); bool const isMapVersionEditable = CanEditMapForPosition(info.GetMercator()); @@ -3263,6 +3264,43 @@ void Framework::FillDescription(FeatureType & ft, place_page::Info & info) const } } +void Framework::CheckPanoramaxImagery(place_page::Info & info) const +{ + // Query features within 50m radius + auto constexpr radiusM = 50.0; + auto const center = info.GetMercator(); + auto const rect = mercator::RectByCenterXYAndSizeInMeters(center, radiusM); + + auto const panoramaxType = classif().GetTypeByPath({"panoramax", "image"}); + + bool hasPanoramax = false; + std::string panoramaxImageId; + std::string panoramaxUrl; + + m_featuresFetcher.GetDataSource().ForEachInRect([&](FeatureType & ft) + { + if (ft.GetTypes().Has(panoramaxType)) + { + auto const imageId = ft.GetMetadata(feature::Metadata::FMD_PANORAMAX); + if (!imageId.empty()) + { + hasPanoramax = true; + panoramaxImageId = std::string(imageId); + panoramaxUrl = "https://panoramax.openstreetmap.fr/#focus=pic:" + panoramaxImageId; + return base::ControlFlow::Break; // Found one, stop searching + } + } + return base::ControlFlow::Continue; + }, rect, df::GetDrawTileScale(rect)); + + if (hasPanoramax) + { + info.m_hasPanoramax = true; + info.m_panoramaxImageId = std::move(panoramaxImageId); + info.m_panoramaxUrl = std::move(panoramaxUrl); + } +} + void Framework::OnPowerFacilityChanged(power_management::Facility const facility, bool enabled) { if (facility == power_management::Facility::PerspectiveView || facility == power_management::Facility::Buildings3d) diff --git a/libs/map/framework.hpp b/libs/map/framework.hpp index 8abb43cbd..fc754df90 100644 --- a/libs/map/framework.hpp +++ b/libs/map/framework.hpp @@ -640,6 +640,7 @@ private: void FillTrackInfo(Track const & track, m2::PointD const & trackPoint, place_page::Info & info) const; void SetPlacePageLocation(place_page::Info & info); void FillDescription(FeatureType & ft, place_page::Info & info) const; + void CheckPanoramaxImagery(place_page::Info & info) const; public: search::ReverseGeocoder::Address GetAddressAtPoint(m2::PointD const & pt) const; diff --git a/libs/map/place_page_info.hpp b/libs/map/place_page_info.hpp index 80d599512..39b6811a9 100644 --- a/libs/map/place_page_info.hpp +++ b/libs/map/place_page_info.hpp @@ -114,6 +114,9 @@ public: bool HasApiUrl() const { return !m_apiUrl.empty(); } /// TODO: Support all possible Internet types in UI. @See MapObject::GetInternet(). bool HasWifi() const { return GetInternet() == feature::Internet::Wlan; } + /// @returns true if Panoramax imagery is available within 50m. + bool HasPanoramax() const { return m_hasPanoramax; } + std::string const & GetPanoramaxUrl() const { return m_panoramaxUrl; } /// Should be used by UI code to generate cool name for new bookmarks. // TODO: Tune new bookmark name. May be add address or some other data. kml::LocalizableString FormatNewBookmarkName() const; @@ -258,6 +261,11 @@ private: /// Formatted feature address for inner using. std::string m_address; + /// Panoramax + bool m_hasPanoramax = false; + std::string m_panoramaxImageId; + std::string m_panoramaxUrl; + /// Routing RouteMarkType m_routeMarkType; size_t m_intermediateIndex = 0; diff --git a/tools/python/maps_generator/generator/env.py b/tools/python/maps_generator/generator/env.py index b02545e99..7c375578a 100644 --- a/tools/python/maps_generator/generator/env.py +++ b/tools/python/maps_generator/generator/env.py @@ -351,6 +351,10 @@ class PathProvider: def addresses_path() -> AnyStr: return settings.ADDRESSES_PATH + @staticmethod + def panoramax_path() -> AnyStr: + return settings.PANORAMAX_PATH + @staticmethod def borders_path() -> AnyStr: return os.path.join(settings.USER_RESOURCE_PATH, "borders") diff --git a/tools/python/maps_generator/generator/settings.py b/tools/python/maps_generator/generator/settings.py index fb4520333..6287b5401 100644 --- a/tools/python/maps_generator/generator/settings.py +++ b/tools/python/maps_generator/generator/settings.py @@ -121,6 +121,7 @@ US_POSTCODES_URL = "" SRTM_PATH = "" ISOLINES_PATH = "" ADDRESSES_PATH = "" +PANORAMAX_PATH = "" # Stats section: STATS_TYPES_CONFIG = os.path.join(ETC_DIR, "stats_types_config.txt") @@ -278,6 +279,7 @@ def init(default_settings_path: AnyStr): global SRTM_PATH global ISOLINES_PATH global ADDRESSES_PATH + global PANORAMAX_PATH PLANET_URL = cfg.get_opt_path("External", "PLANET_URL", PLANET_URL) PLANET_MD5_URL = cfg.get_opt_path("External", "PLANET_MD5_URL", md5_ext(PLANET_URL)) @@ -306,6 +308,7 @@ def init(default_settings_path: AnyStr): SRTM_PATH = cfg.get_opt_path("External", "SRTM_PATH", SRTM_PATH) ISOLINES_PATH = cfg.get_opt_path("External", "ISOLINES_PATH", ISOLINES_PATH) ADDRESSES_PATH = cfg.get_opt_path("External", "ADDRESSES_PATH", ADDRESSES_PATH) + PANORAMAX_PATH = cfg.get_opt_path("External", "PANORAMAX_PATH", PANORAMAX_PATH) # Stats section: global STATS_TYPES_CONFIG diff --git a/tools/python/maps_generator/generator/stages_declaration.py b/tools/python/maps_generator/generator/stages_declaration.py index 06756a3d7..f268af94d 100644 --- a/tools/python/maps_generator/generator/stages_declaration.py +++ b/tools/python/maps_generator/generator/stages_declaration.py @@ -134,6 +134,8 @@ class StageFeatures(Stage): if is_accepted(env, StageIsolinesInfo): extra.update({"isolines_path": PathProvider.isolines_path()}) extra.update({"addresses_path": PathProvider.addresses_path()}) + if PathProvider.panoramax_path(): + extra.update({"panoramax_path": PathProvider.panoramax_path()}) steps.step_features(env, **extra) if os.path.exists(env.paths.packed_polygons_path): diff --git a/tools/python/maps_generator/var/etc/map_generator.ini.prod b/tools/python/maps_generator/var/etc/map_generator.ini.prod index 036893991..f287a0ffe 100644 --- a/tools/python/maps_generator/var/etc/map_generator.ini.prod +++ b/tools/python/maps_generator/var/etc/map_generator.ini.prod @@ -80,6 +80,7 @@ SUBWAY_URL: file:///home/planet/subway/subways.transit.json SRTM_PATH: /home/planet/SRTM-patched-europe/ ISOLINES_PATH: /home/planet/isolines/ ADDRESSES_PATH: /home/planet/tiger/ +PANORAMAX_PATH: /home/planet/panoramax/countries/ # Local path (not url!) to .csv files. UK_POSTCODES_URL: /home/planet/postcodes/gb-postcode-data/gb_postcodes.csv diff --git a/tools/unix/maps/docker_maps_generator.sh b/tools/unix/maps/docker_maps_generator.sh index db04170a8..470909e3a 100644 --- a/tools/unix/maps/docker_maps_generator.sh +++ b/tools/unix/maps/docker_maps_generator.sh @@ -13,6 +13,7 @@ mkdir -p /home/planet/postcodes/gb-postcode-data/ mkdir -p /home/planet/postcodes/us-postcodes/ mkdir -p /home/planet/SRTM-patched-europe/ mkdir -p /home/planet/subway +mkdir -p /home/planet/panoramax/countries/ echo "<$(date +%T)> Running ./configure.sh ..." cd ~/comaps