/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2025, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a license // agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// #include "OdGeoMapProviderInfo.h" #include "OdGeoMapCurlRequstHelper.h" #include "OdDbGeoMapHelper.h" #include "OdGeneralMaps.h" #include "RxRasterServices.h" #include "FlatMemStream.h" #include OdAnsiString bingTileXYToQuadKey(OdUInt8 uLOD, OdUInt32 uTileX, OdUInt32 uTileY) { OdAnsiString sQuadKey; for (OdUInt8 i = uLOD; i > 0; --i) { char cDigit = '0'; int mask = 1 << (i - 1); if ((uTileX & mask) != 0) { cDigit++; } if ((uTileY & mask) != 0) { cDigit += 2; } sQuadKey += cDigit; } return sQuadKey; } //OdGeoMapProviderInfo OdGeoMapProviderInfo::OdGeoMapProviderInfo(const OdAnsiString& sKey, OdUInt32 uTileSize, OdUInt8 uMinLOD, OdUInt8 uMaxLOD) : m_sKey(sKey) , m_uTileSize(uTileSize) , m_uMinLOD(uMinLOD) , m_uMaxLOD(uMaxLOD) { } OdGeoMapProviderInfo::~OdGeoMapProviderInfo() { } OdResult OdGeoMapProviderInfo::getTileInfo(OdUInt32& uTileSize, OdUInt8& uMinLOD, OdUInt8& uMaxLOD) const { uTileSize = m_uTileSize; uMinLOD = m_uMinLOD; uMaxLOD = m_uMaxLOD; return eOk; } //OdGeoMapBingProviderInfo OdGeoMapBingProviderInfo::OdGeoMapBingProviderInfo(const OdAnsiString& sKey, OdGeoMapType eType) : OdGeoMapProviderInfo(sKey, 256, 1, 21) , m_eType(eType) { } OdGeoMapBingProviderInfo::~OdGeoMapBingProviderInfo() { } OdResult OdGeoMapBingProviderInfo::getTileUrl(OdUInt8 uLOD, OdUInt32 uTileX, OdUInt32 uTileY, OdAnsiString& sUrl) const { OdAnsiString sQuadKey = bingTileXYToQuadKey(uLOD, uTileX, uTileY); switch (m_eType) { case kBingAerial: sUrl.format("https://tiles.virtualearth.net/tiles/a%s?g=1&shading=hill&n=z", sQuadKey.c_str()); break; case kBingRoad: sUrl.format("https://tiles.virtualearth.net/tiles/r%s?g=1&shading=hill&n=z", sQuadKey.c_str()); break; case kBingHybrid: sUrl.format("https://tiles.virtualearth.net/tiles/h%s?g=1&shading=hill&n=z", sQuadKey.c_str()); break; default: sUrl.empty(); return eNotApplicable; } return eOk; } OdResult OdGeoMapBingProviderInfo::loadMetadata(bool bUpdate) { if (!bUpdate && !m_metadata.isEmpty()) { return eOk; } if (m_sKey.isEmpty()) { return eNotApplicable; } OdAnsiString sImageryProvidersUrl; switch (m_eType) { case kBingAerial: sImageryProvidersUrl = "https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?incl=ImageryProviders&o=json&key="; break; case kBingRoad: sImageryProvidersUrl = "https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Road?incl=ImageryProviders&o=json&key="; break; case kBingHybrid: sImageryProvidersUrl = "https://dev.virtualearth.net/REST/v1/Imagery/Metadata/AerialWithLabels?incl=ImageryProviders&o=json&key="; break; default: return eNotApplicable; } sImageryProvidersUrl += m_sKey; m_metadata.clear(); OdResult status = OdGeoMapCurlRequstHelper::simpleRequst(sImageryProvidersUrl, m_metadata); if (eOk != status) { m_metadata.clear(); return status; } m_metadata.push_back(0); //as end of line return eOk; } OdResult OdGeoMapBingProviderInfo::getCopyrightStrings(const OdDbGeoData* pGeoData, OdUInt8 uLOD, const OdGePoint2dArray& arrImageArea, OdStringArray& arrCopyrightStrings) { OdResult status = loadMetadata(false); if (eOk != status) { return status; } rapidjson::Document doc; doc.Parse((const char*)m_metadata.asArrayPtr()); if (doc.HasParseError() || !doc.HasMember("resourceSets")) { return eInvalidInput; } const rapidjson::Value& resourceSets = doc["resourceSets"]; if (!resourceSets.IsArray()) { return eInvalidInput; } double dZ = pGeoData->referencePoint().z; OdGePoint2dArray arrExtents; arrExtents.resize(4); OdGePoint2dArray arrExtentsRes; OdGePoint2d ptIntersect; OdDbGeoCoordinateSystemPtr pCS; if (eOk != OdDbGeoCoordinateSystem::create(pGeoData->coordinateSystem(), pCS) || pCS.isNull()) { return eInvalidInput; } OdGeExtents2d geoExts; pCS->getGeodeticExtents(geoExts); for (rapidjson::SizeType i = 0; i < resourceSets.Size(); ++i) { const rapidjson::Value& resourceSetsElement = resourceSets[i]; if (!resourceSetsElement.HasMember("resources")) { continue; } const rapidjson::Value& resources = resourceSetsElement["resources"]; if (!resources.IsArray()) { continue; } for (rapidjson::SizeType j = 0; j < resources.Size(); ++j) { const rapidjson::Value& resourcesElement = resources[j]; if (!resourcesElement.HasMember("imageryProviders")) { continue; } const rapidjson::Value& imageryProviders = resourcesElement["imageryProviders"]; if (!imageryProviders.IsArray()) { continue; } for (rapidjson::SizeType k = 0; k < imageryProviders.Size(); ++k) { const rapidjson::Value& imageryProvidersElement = imageryProviders[k]; if (!imageryProvidersElement.HasMember("coverageAreas") || !imageryProvidersElement.HasMember("attribution")) { continue; } const rapidjson::Value& coverageAreas = imageryProvidersElement["coverageAreas"]; if (!coverageAreas.IsArray()) { continue; } for (rapidjson::SizeType m = 0; m < coverageAreas.Size(); ++m) { const rapidjson::Value& coverageAreasElement = coverageAreas[m]; if (!coverageAreasElement.HasMember("zoomMin") || !coverageAreasElement.HasMember("zoomMax") || !coverageAreasElement.HasMember("bbox")) { continue; } const rapidjson::Value& zoomMin = coverageAreasElement["zoomMin"]; const rapidjson::Value& zoomMax = coverageAreasElement["zoomMax"]; if (!zoomMin.IsInt() || !zoomMax.IsInt() || uLOD < zoomMin.GetInt() || uLOD > zoomMax.GetInt()) { continue; } const rapidjson::Value& bbox = coverageAreasElement["bbox"]; if (!bbox.IsArray() || 4 != bbox.Size() || !bbox[0].IsNumber() || !bbox[1].IsNumber() || !bbox[2].IsNumber() || !bbox[3].IsNumber()) { continue; } //current CS extent correction double dSouthLatitude = OdGeneralMaps::clip(bbox[0].GetDouble(), geoExts.minPoint().y, geoExts.maxPoint().y); double dWestLongitude = OdGeneralMaps::clip(bbox[1].GetDouble(), geoExts.minPoint().x, geoExts.maxPoint().x); double dNorthLatitude = OdGeneralMaps::clip(bbox[2].GetDouble(), geoExts.minPoint().y, geoExts.maxPoint().y); double dEastLongitude = OdGeneralMaps::clip(bbox[3].GetDouble(), geoExts.minPoint().x, geoExts.maxPoint().x); arrExtents[0].set(dWestLongitude, dSouthLatitude); arrExtents[1].set(dEastLongitude, dSouthLatitude); arrExtents[2].set(dEastLongitude, dNorthLatitude); arrExtents[3].set(dWestLongitude, dNorthLatitude); OdDbGeoMapHelper::createLLAExtentsBy4Points(arrExtents, arrExtentsRes); OdGePoint3d ptBuf; for (OdUInt32 l = 0; l < arrExtentsRes.size(); ++l) { pGeoData->transformFromLonLatAlt(OdGePoint3d(arrExtentsRes[l].x, arrExtentsRes[l].y, dZ), ptBuf); arrExtentsRes[l] = ptBuf.convert2d(); } if (OdDbGeoMapHelper::getPolygonsPoint(arrExtentsRes, arrImageArea, ptIntersect)) { const rapidjson::Value& attribution = imageryProvidersElement["attribution"]; if (attribution.IsString()) { OdAnsiString sImageryProvider = attribution.GetString(); OdCharArray arrBuf; OdCharMapper::utf8ToUnicode(sImageryProvider.c_str(), sImageryProvider.getLength(), arrBuf); arrCopyrightStrings.append(arrBuf.getPtr()); break; } } } } } } return eOk; } OdResult OdGeoMapBingProviderInfo::getBrandLogo(OdGiRasterImagePtr& pRasterLogo, bool bUpdate) { if (bUpdate || m_pLogo.isNull()) { //BrandLogoUri - link from metadata, but it's always the same: OdBinaryData logo; OdAnsiString imageUrl("https://dev.virtualearth.net/Branding/logo_powered_by.png"); OdResult res = OdGeoMapCurlRequstHelper::simpleRequst(imageUrl, logo); if (eOk != res) { return res; } try { OdRxRasterServicesPtr pRasterSvcs = odrxDynamicLinker()->loadModule(RX_RASTER_SERVICES_APPNAME); OdFlatMemStreamPtr pStreamLogo = OdFlatMemStream::createNew((void*)logo.asArrayPtr(), logo.size()); m_pLogo = pRasterSvcs->loadRasterImage(pStreamLogo); } catch (...) { //in case of error responce return eInvalidInput; } } pRasterLogo = m_pLogo->clone(); return eOk; } //OdGeoMapAzureProviderInfo OdGeoMapAzureProviderInfo::OdGeoMapAzureProviderInfo(const OdAnsiString& sKey, OdUInt32 uTileSize, OdUInt8 uMinLOD, OdUInt8 uMaxLOD, OdGeoMapType eType) : OdGeoMapProviderInfo(sKey, uTileSize, uMinLOD, uMaxLOD) , m_eType(eType) { } OdGeoMapAzureProviderInfo::~OdGeoMapAzureProviderInfo() { } OdResult OdGeoMapAzureProviderInfo::getBrandLogo(OdGiRasterImagePtr& /*pRasterLogo*/, bool /*bUpdate*/) { //no image return eNotApplicable; } OdResult OdGeoMapAzureProviderInfo::getTileUrl(OdUInt8 uLOD, OdUInt32 uTileX, OdUInt32 uTileY, OdAnsiString& sUrl) const { if (m_sKey.isEmpty()) { return eNotApplicable; } switch (m_eType) { case kAerial: sUrl.format("https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=microsoft.imagery&zoom=%ld&x=%ld&y=%ld&subscription-key=%s", uLOD, uTileX, uTileY, m_sKey.c_str()); break; case kRoad: sUrl.format("https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=microsoft.base.road&zoom=%ld&x=%ld&y=%ld&subscription-key=%s", uLOD, uTileX, uTileY, m_sKey.c_str()); break; case kHybrid: sUrl.format("https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=microsoft.base.hybrid.road&zoom=%ld&x=%ld&y=%ld&subscription-key=%s", uLOD, uTileX, uTileY, m_sKey.c_str()); break; default: return eNotApplicable; } return eOk; } OdResult OdGeoMapAzureProviderInfo::loadMetadata(bool bUpdate) { if (!bUpdate && !m_metadata.isEmpty()) { return eOk; } if (m_sKey.isEmpty()) { return eNotApplicable; } OdAnsiString sImageryProvidersUrl; switch (m_eType) { case kAerial: sImageryProvidersUrl = "https://atlas.microsoft.com/map/tileset?api-version=2024-04-01&tilesetId=microsoft.imagery&subscription-key="; break; case kRoad: sImageryProvidersUrl = "https://atlas.microsoft.com/map/tileset?api-version=2024-04-01&tilesetId=microsoft.base.road&subscription-key="; break; case kHybrid: sImageryProvidersUrl = "https://atlas.microsoft.com/map/tileset?api-version=2024-04-01&tilesetId=microsoft.base.hybrid.road&subscription-key="; break; default: return eNotApplicable; } sImageryProvidersUrl += m_sKey; m_metadata.clear(); OdResult status = OdGeoMapCurlRequstHelper::simpleRequst(sImageryProvidersUrl, m_metadata); if (eOk != status) { m_metadata.clear(); return status; } m_metadata.push_back(0); //as end of line return eOk; } OdResult OdGeoMapAzureProviderInfo::getCopyrightStrings(const OdDbGeoData* /*pGeoData*/, OdUInt8 /*uLOD*/, const OdGePoint2dArray& /*arrImageArea*/, OdStringArray& arrCopyrightStrings) { OdResult status = loadMetadata(false); if (eOk != status) { return status; } rapidjson::Document doc; doc.Parse((const char*)m_metadata.asArrayPtr()); if (doc.HasParseError() || !doc.HasMember("attribution")) { return eInvalidInput; } const rapidjson::Value& copyrightText = doc["attribution"]; if (copyrightText.IsString()) { OdString sCopyrightString = copyrightText.GetString(); //represented as: " © copyright " © - (c) int nStartPos = sCopyrightString.find(L'>'); if (-1 != nStartPos) { int nEndPos = sCopyrightString.find(L'<', nStartPos + 1); if (-1 != nEndPos) { sCopyrightString = sCopyrightString.mid(nStartPos + 1, nEndPos - nStartPos - 1); sCopyrightString.replace(L"©", L"\u00A9"); arrCopyrightStrings.push_back(sCopyrightString); } } } arrCopyrightStrings.push_back(L"\u00A9 Azure Maps"); return eOk; } //OdGeoMapEsriProviderInfo OdGeoMapEsriProviderInfo::OdGeoMapEsriProviderInfo(const OdAnsiString& sKey, OdUInt32 uTileSize, OdUInt8 uMinLOD, OdUInt8 uMaxLOD, OdGeoMapType eType) : OdGeoMapProviderInfo(sKey, uTileSize, uMinLOD, uMaxLOD) , m_eType(eType) { } OdGeoMapEsriProviderInfo::~OdGeoMapEsriProviderInfo() { } OdResult OdGeoMapEsriProviderInfo::getTileUrl(OdUInt8 uLOD, OdUInt32 uTileX, OdUInt32 uTileY, OdAnsiString& sUrl) const { if (m_sKey.isEmpty()) { return eNotApplicable; } switch (m_eType) { case kEsriImagery: sUrl.format("https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/%ld/%ld/%ld?token=%s", uLOD, uTileY, uTileX, m_sKey.c_str()); break; case kEsriOpenStreetMap: sUrl.format("https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/standard/static/tile/%ld/%ld/%ld?token=%s", uLOD, uTileY, uTileX, m_sKey.c_str()); break; case kEsriStreets: sUrl.format("https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/streets/static/tile/%ld/%ld/%ld?token=%s", uLOD, uTileY, uTileX, m_sKey.c_str()); break; case kEsriLightGray: sUrl.format("https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/light-gray/static/tile/%ld/%ld/%ld?token=%s", uLOD, uTileY, uTileX, m_sKey.c_str()); break; case kEsriDarkGray: sUrl.format("https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/dark-gray/static/tile/%ld/%ld/%ld?token=%s", uLOD, uTileY, uTileX, m_sKey.c_str()); break; default: sUrl.empty(); return eNotApplicable; } return eOk; } OdResult OdGeoMapEsriProviderInfo::loadMetadata(bool bUpdate) { if (!bUpdate && !m_metadata.isEmpty()) { return eOk; } if (m_sKey.isEmpty()) { return eNotApplicable; } OdAnsiString sImageryProvidersUrl; switch (m_eType) { case kEsriImagery: sImageryProvidersUrl = "https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/arcgis/imagery/standard?token="; break; case kEsriOpenStreetMap: sImageryProvidersUrl = "https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/standard/static?token="; break; case kEsriStreets: sImageryProvidersUrl = "https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/streets/static?token="; break; case kEsriLightGray: sImageryProvidersUrl = "https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/light-gray/static?token="; break; case kEsriDarkGray: sImageryProvidersUrl = "https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-map-tiles-service/v1/osm/dark-gray/static?token="; break; default: return eNotApplicable; } sImageryProvidersUrl += m_sKey; m_metadata.clear(); OdResult status = OdGeoMapCurlRequstHelper::simpleRequst(sImageryProvidersUrl, m_metadata); if (eOk != status) { m_metadata.clear(); return status; } m_metadata.push_back(0); //as end of line return eOk; } OdResult OdGeoMapEsriProviderInfo::getBrandLogo(OdGiRasterImagePtr& /*pRasterLogo*/, bool /*bUpdate*/) { //no image return eNotApplicable; } //OdGeoMapEsriImageryProviderInfo OdGeoMapEsriImageryProviderInfo::OdGeoMapEsriImageryProviderInfo(const OdAnsiString& sKey, OdGeoMapType eType) : OdGeoMapEsriProviderInfo(sKey, 256, 0, 23, eType) { } OdGeoMapEsriImageryProviderInfo::~OdGeoMapEsriImageryProviderInfo() { } OdResult OdGeoMapEsriImageryProviderInfo::getCopyrightStrings(const OdDbGeoData* /*pGeoData*/, OdUInt8 /*uLOD*/, const OdGePoint2dArray& /*arrImageArea*/, OdStringArray& arrCopyrightStrings) { OdResult status = loadMetadata(false); if (eOk != status) { return status; } rapidjson::Document doc; doc.Parse((const char*)m_metadata.asArrayPtr()); if (doc.HasParseError() || !doc.HasMember("sources")) { return eInvalidInput; } const rapidjson::Value& sources = doc["sources"]; if (!sources.IsObject()) { return eInvalidInput; } for (rapidjson::Value::ConstMemberIterator pIt = sources.MemberBegin(); pIt != sources.MemberEnd(); ++pIt) { const rapidjson::Value& val = pIt->value; if (!val.IsObject() || !val.HasMember("copyrightText")) { continue; } const rapidjson::Value& copyrightText = val["copyrightText"]; if (!copyrightText.IsString()) { continue; } arrCopyrightStrings.push_back(copyrightText.GetString()); } arrCopyrightStrings.push_back("Powered by Esri"); return eOk; } //OdGeoMapEsriOSMProviderInfo OdGeoMapEsriOSMProviderInfo::OdGeoMapEsriOSMProviderInfo(const OdAnsiString& sKey, OdGeoMapType eType) : OdGeoMapEsriProviderInfo(sKey, 512, 0, 22, eType) { } OdGeoMapEsriOSMProviderInfo::~OdGeoMapEsriOSMProviderInfo() { } OdResult OdGeoMapEsriOSMProviderInfo::getCopyrightStrings(const OdDbGeoData* /*pGeoData*/, OdUInt8 /*uLOD*/, const OdGePoint2dArray& /*arrImageArea*/, OdStringArray& arrCopyrightStrings) { OdResult status = loadMetadata(false); if (eOk != status) { return status; } rapidjson::Document doc; doc.Parse((const char*)m_metadata.asArrayPtr()); if (doc.HasParseError() || !doc.HasMember("copyrightText")) { return eInvalidInput; } const rapidjson::Value& copyrightText = doc["copyrightText"]; if (copyrightText.IsString()) { arrCopyrightStrings.push_back(copyrightText.GetString()); } arrCopyrightStrings.push_back("Powered by Esri"); return eOk; }