std::shared_ptr<OsmAnd::MapMarker> OsmAnd::MapMarkerBuilder_P::buildAndAddToCollection(
    const std::shared_ptr<MapMarkersCollection>& collection)
{
    QReadLocker scopedLocker(&_lock);

    // Construct map symbols group for this marker
    const std::shared_ptr<MapMarker> marker(new MapMarker(
        _markerId,
        _baseOrder,
        _pinIcon,
        _pinIconVerticalAlignment,
        _pinIconHorisontalAlignment,
        detachedOf(_onMapSurfaceIcons),
        _isAccuracyCircleSupported,
        _accuracyCircleBaseColor));
    
    marker->setIsHidden(_isHidden);
    if (_isAccuracyCircleSupported)
    {
        marker->setIsAccuracyCircleVisible(_isAccuracyCircleVisible);
        marker->setAccuracyCircleRadius(_accuracyCircleRadius);
    }
    marker->setPosition(_position);
    marker->setPinIconModulationColor(_pinIconModulationColor);
    
    marker->applyChanges();

    // Add marker to collection and return it if adding was successful
    if (!collection->_p->addMarker(marker))
        return nullptr;
    return marker;
}
bool OsmAnd::MapRendererTiledResourcesCollection::updateCollectionSnapshot() const
{
    const auto invalidatesDiscarded = _collectionSnapshotInvalidatesCount.fetchAndAddOrdered(0);
    if (invalidatesDiscarded == 0)
        return true;

    // Copy from original storage to temp storage
    Storage storageCopy;
    {
        if (!_collectionLock.tryLockForRead())
            return false;

        for (int zoomLevel = MinZoomLevel; zoomLevel <= MaxZoomLevel; zoomLevel++)
        {
            const auto& sourceStorageLevel = constOf(_storage)[zoomLevel];
            auto& targetStorageLevel = storageCopy[zoomLevel];

            targetStorageLevel = detachedOf(sourceStorageLevel);
        }

        _collectionLock.unlock();
    }
    _collectionSnapshotInvalidatesCount.fetchAndAddOrdered(-invalidatesDiscarded);

    // Copy from temp storage to snapshot
    {
        QWriteLocker scopedLocker(&_snapshot->_lock);

        _snapshot->_storage = qMove(storageCopy);
    }

    return true;
}
QHash< OsmAnd::MapMarker::OnSurfaceIconKey, std::shared_ptr<const SkBitmap> >
OsmAnd::MapMarkerBuilder_P::getOnMapSurfaceIcons() const
{
    QReadLocker scopedLocker(&_lock);

    return detachedOf(_onMapSurfaceIcons);
}
QHash< OsmAnd::ResolvedMapStyle::ValueDefinitionId, OsmAnd::MapStyleConstantValue > OsmAnd::MapPresentationEnvironment_P::getSettings() const
{
    QMutexLocker scopedLocker(&_settingsChangeMutex);

    return detachedOf(_settings);
}
QList< QList<OsmAnd::MapObjectsSymbolsProvider_P::ComputedPinPoint> > OsmAnd::MapObjectsSymbolsProvider_P::computePinPoints(
    const QVector<PointI>& path31,
    const float globalLeftPaddingInPixels,
    const float globalRightPaddingInPixels,
    const float globalSpacingBetweenBlocksInPixels,
    const QList<SymbolForPinPointsComputation>& symbolsForPinPointsComputation,
    const ZoomLevel minZoom,
    const ZoomLevel maxZoom,
    const ZoomLevel neededZoom) const
{
    QList< QList<ComputedPinPoint> > computedPinPointsByLayer;

    // Compute pin-points placement starting from minZoom to maxZoom. How this works:
    //
    // Example of block instance placement assuming it fits exactly 4 times on minZoom ('bi' is block instance):
    // minZoom+0: bibibibi
    // Since each next zoom is 2x bigger, thus here's how minZoom+1 will look like without additional instances ('-' is widthOfBlockInPixels/2)
    // minZoom+1: -bi--bi--bi--bi-
    // After placing 3 additional instances it will look like
    // minZoom+1: -bibibibibibibi-
    // On next zoom without additional instances it will look like
    // minZoom+2: ---bi--bi--bi--bi--bi--bi--bi---
    // After placing additional 8 instances it will look like
    // minZoom+2: -bibibibibibibibibibibibibibibi-
    // On next zoom without additional 16 instances it will look like
    // minZoom+3: ---bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi---
    // On next zoom without additional 32 instances it will look like
    // minZoom+4: ---bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi---
    //
    // Another example of block instance placement assuming only 3.5 block instances fit ('.' is widthOfBlockInPixels/4)
    // minZoom+0: .bibibi.
    // On next zoom without 4 additional instances
    // minZoom+1: --bi--bi--bi--
    // After placement additional 4 instances
    // minZoom+1: bibibibibibibi
    // On next zoom without additional 6 instances
    // minZoom+2: -bi--bi--bi--bi--bi--bi--bi-
    // minZoom+2: -bibibibibibibibibibibibibi-
    // On next zoom without additional 14 instances
    // minZoom+3: ---bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi--bi---
    //

    // Step 0. Initial checks
    if (symbolsForPinPointsComputation.isEmpty())
        return computedPinPointsByLayer;

    // Step 1. Get scale factor from 31 to pixels for minZoom.
    // Length on path in pixels depends on tile size in pixels, and density
    const auto tileSize31 = (1u << (ZoomLevel::MaxZoomLevel - minZoom));
    const auto from31toPixelsScale = static_cast<double>(owner->referenceTileSizeOnScreenInPixels) / tileSize31;

    // Step 2. Compute path length, path segments length (in 31 and in pixels)
    const auto pathSize = path31.size();
    const auto pathSegmentsCount = pathSize - 1;
    float pathLengthInPixels = 0.0f;
    QVector<float> pathSegmentsLengthInPixelsOnBaseZoom(pathSegmentsCount);
    auto pPathSegmentLengthInPixelsOnBaseZoom = pathSegmentsLengthInPixelsOnBaseZoom.data();
    double pathLength31 = 0.0;
    QVector<double> pathSegmentsLength31(pathSegmentsCount);
    auto pPathSegmentLength31 = pathSegmentsLength31.data();
    auto pPoint31 = path31.constData();
    auto pPrevPoint31 = pPoint31++;
    auto basePathPointIndex = 0;
    auto globalPaddingFromBasePathPoint = 0.0f;
    bool capturedBasePathPointIndex = false;
    for (auto segmentIdx = 0; segmentIdx < pathSegmentsCount; segmentIdx++)
    {
        const auto segmentLength31 = qSqrt((*(pPoint31++) - *(pPrevPoint31++)).squareNorm());
        *(pPathSegmentLength31++) = segmentLength31;
        pathLength31 += segmentLength31;

        const auto segmentLengthInPixels = segmentLength31 * from31toPixelsScale;
        *(pPathSegmentLengthInPixelsOnBaseZoom++) = segmentLengthInPixels;
        pathLengthInPixels += segmentLengthInPixels;

        if (pathLength31 > globalLeftPaddingInPixels && !capturedBasePathPointIndex)
        {
            basePathPointIndex = segmentIdx;
            if (!qFuzzyIsNull(globalLeftPaddingInPixels))
                globalPaddingFromBasePathPoint = globalLeftPaddingInPixels - (pathLengthInPixels - segmentLengthInPixels);

            capturedBasePathPointIndex = true;
        }
    }
    const auto usablePathLengthInPixels = pathLengthInPixels - globalLeftPaddingInPixels - globalRightPaddingInPixels;
    if (usablePathLengthInPixels <= 0.0f)
        return computedPinPointsByLayer;

    // Step 3. Compute total width of all symbols requested. This will be the block width.
    const auto symbolsCount = symbolsForPinPointsComputation.size();
    QVector<float> symbolsFullSizesInPixels(symbolsCount);
    auto pSymbolFullSizeInPixels = symbolsFullSizesInPixels.data();
    float blockWidthWithoutSpacing = 0.0f;
    for (const auto& symbolForPinPointsComputation : constOf(symbolsForPinPointsComputation))
    {
        auto symbolWidth = 0.0f;
        symbolWidth += symbolForPinPointsComputation.leftPaddingInPixels;
        symbolWidth += symbolForPinPointsComputation.widthInPixels;
        symbolWidth += symbolForPinPointsComputation.rightPaddingInPixels;
        *(pSymbolFullSizeInPixels++) = symbolWidth;
        blockWidthWithoutSpacing += symbolWidth;
    }
    if (symbolsForPinPointsComputation.isEmpty() || qFuzzyIsNull(blockWidthWithoutSpacing))
        return computedPinPointsByLayer;
    const auto blockWidthWithSpacing = blockWidthWithoutSpacing + globalSpacingBetweenBlocksInPixels;

    // Step 4. Process values for base zoom level
    const auto lengthOfPathInPixelsOnBaseZoom = usablePathLengthInPixels;

    // Step 5. Process by zoom levels
    auto lengthOfPathInPixelsOnCurrentZoom = lengthOfPathInPixelsOnBaseZoom;
    auto pathSegmentsLengthInPixelsOnCurrentZoom = detachedOf(pathSegmentsLengthInPixelsOnBaseZoom);
    auto totalNumberOfCompleteBlocks = 0;
    auto remainingPathLengthOnPrevZoom = 0.0f;
    auto kOffsetToFirstBlockOnPrevZoom = 0.0f;
    auto globalPaddingInPixelsFromBasePathPointOnCurrentZoom = globalPaddingFromBasePathPoint;
    for (int currentZoomLevel = minZoom; currentZoomLevel <= maxZoom; currentZoomLevel++)
    {
        // Compute how many new blocks will fit, where and how to place them
        auto blocksToInstantiate = 0;
        auto kOffsetToFirstNewBlockOnCurrentZoom = 0.0f;
        auto kOffsetToFirstPresentBlockOnCurrentZoom = 0.0f;
        auto fullSizeOfSymbolsThatFit = 0.0f; // This is used only in case even 1 block doesn't fit
        auto numberOfSymbolsThatFit = 0; // This is used only in case even 1 block doesn't fit
        if (totalNumberOfCompleteBlocks == 0)
        {
            if (lengthOfPathInPixelsOnCurrentZoom >= blockWidthWithoutSpacing)
            {
                const auto numberOfBlocksThatFit = lengthOfPathInPixelsOnCurrentZoom / blockWidthWithoutSpacing;
                blocksToInstantiate = qMax(qFloor(numberOfBlocksThatFit), 1);
                kOffsetToFirstNewBlockOnCurrentZoom = (numberOfBlocksThatFit - static_cast<int>(numberOfBlocksThatFit)) / 2.0f;
                kOffsetToFirstPresentBlockOnCurrentZoom = -1.0f;
                //if (lengthOfPathInPixelsOnCurrentZoom >= blockWidth + globalSpacingBetweenBlocksInPixels + blockWidth)
                //{
                //    // If more than 2 blocks + spacing between them
                //    const auto numberOfBlocksThatFit = lengthOfPathInPixelsOnCurrentZoom / blockWidthWithSpacing;
                //    blocksToInstantiate = qMax(qFloor(numberOfBlocksThatFit), 1);
                //    kOffsetToFirstNewBlockOnCurrentZoom = (numberOfBlocksThatFit - static_cast<int>(numberOfBlocksThatFit)) / 2.0f;
                //}
                //else
                //{
                //    // If only 1 block (with or without spacing)
                //    blocksToInstantiate = 1;
                //    kOffsetToFirstNewBlockOnCurrentZoom = (numberOfBlocksThatFit - static_cast<int>(numberOfBlocksThatFit)) / 2.0f;
                //}
                //kOffsetToFirstPresentBlockOnCurrentZoom = -1.0f;
            }
            else
            {
                blocksToInstantiate = 0;
                for (auto symbolIdx = 0; symbolIdx < symbolsCount; symbolIdx++)
                {
                    const auto& symbolFullSize = symbolsFullSizesInPixels[symbolIdx];

                    if (fullSizeOfSymbolsThatFit + symbolFullSize > lengthOfPathInPixelsOnCurrentZoom)
                        break;

                    fullSizeOfSymbolsThatFit += symbolFullSize;
                    numberOfSymbolsThatFit++;
                }

                // Actually offset to incomplete block
                kOffsetToFirstNewBlockOnCurrentZoom = ((lengthOfPathInPixelsOnCurrentZoom - fullSizeOfSymbolsThatFit) / blockWidthWithoutSpacing) / 2.0f;
                kOffsetToFirstPresentBlockOnCurrentZoom = -1.0f;
            }
        }
        else
        {
            kOffsetToFirstPresentBlockOnCurrentZoom = kOffsetToFirstBlockOnPrevZoom * 2.0f + 0.5f; // 0.5f represents shift of present instance due to x2 scale-up
            blocksToInstantiate = qRound((lengthOfPathInPixelsOnCurrentZoom / blockWidthWithoutSpacing) - 2.0f * kOffsetToFirstPresentBlockOnCurrentZoom) - totalNumberOfCompleteBlocks;
            assert(blocksToInstantiate >= 0);
            if (kOffsetToFirstPresentBlockOnCurrentZoom > 1.0f)
            {
                // Insert new block before present. This will add 2 extra blocks on both sides
                kOffsetToFirstNewBlockOnCurrentZoom = kOffsetToFirstPresentBlockOnCurrentZoom - 1.0f;
                blocksToInstantiate += 2;
            }
            else
            {
                // Insert new block after present
                kOffsetToFirstNewBlockOnCurrentZoom = kOffsetToFirstPresentBlockOnCurrentZoom + 1.0f;
            }
            if (blocksToInstantiate == 0)
                kOffsetToFirstNewBlockOnCurrentZoom = -1.0f;
        }
        const auto remainingPathLengthOnCurrentZoom = lengthOfPathInPixelsOnCurrentZoom - (totalNumberOfCompleteBlocks + blocksToInstantiate) * blockWidthWithoutSpacing;
        const auto offsetToFirstNewBlockInPixels = kOffsetToFirstNewBlockOnCurrentZoom * blockWidthWithoutSpacing;
        const auto eachNewBlockAfterFirstOffsetInPixels = (totalNumberOfCompleteBlocks > 0 ? 2.0f : 1.0f) * blockWidthWithoutSpacing;

#if OSMAND_LOG_SYMBOLS_PIN_POINTS_COMPUTATION
        LogPrintf(LogSeverityLevel::Debug,
            "%d->%f/(%d of [%d;%d]):"
            "\n\ttotalNumberOfCompleteBlocks=%d"
            "\n\tremainingPathLengthOnPrevZoom=%f"
            "\n\tkOffsetToFirstBlockOnPrevZoom=%f"
            "\n\tblocksToInstantiate=%d"
            "\n\tkOffsetToFirstNewBlockOnCurrentZoom=%f"
            "\n\tkOffsetToFirstPresentBlockOnCurrentZoom=%f"
            "\n\tfullSizeOfSymbolsThatFit=%f"
            "\n\tnumberOfSymbolsThatFit=%d"
            "\n\tlengthOfPathInPixelsOnCurrentZoom=%f"
            "\n\tblockWidth=%f"
            /*"\n\t=%f"*/,
            symbolsForPinPointsComputation.size(),
            pathLength31,
            currentZoomLevel,
            minZoom,
            maxZoom,
            totalNumberOfCompleteBlocks,
            remainingPathLengthOnPrevZoom,
            kOffsetToFirstBlockOnPrevZoom,
            blocksToInstantiate,
            kOffsetToFirstNewBlockOnCurrentZoom,
            kOffsetToFirstPresentBlockOnCurrentZoom,
            fullSizeOfSymbolsThatFit,
            numberOfSymbolsThatFit,
            lengthOfPathInPixelsOnCurrentZoom,
            blockWidthWithoutSpacing);
#endif // OSMAND_LOG_SYMBOLS_PIN_POINTS_COMPUTATION

        // Compute actual pin-points only for zoom levels less detained that needed, including needed
        if (currentZoomLevel <= neededZoom)
        {
            // In case at least 1 block fits, only complete blocks are being used.
            // Otherwise, plot only part of symbols (virtually, smaller block)
            if (blocksToInstantiate > 0)
            {
                QList<ComputedPinPoint> computedPinPoints;
                unsigned int scanOriginPathPointIndex = basePathPointIndex;
                float scanOriginPathPointOffsetInPixels = globalPaddingInPixelsFromBasePathPointOnCurrentZoom;
                for (auto blockIdx = 0; blockIdx < blocksToInstantiate; blockIdx++)
                {
                    // Compute base pin-point of block. Actually blocks get pinned,
                    // symbols inside block just receive offset from base pin-point
                    ComputedPinPoint computedBlockPinPoint;
                    unsigned int nextScanOriginPathPointIndex;
                    float nextScanOriginPathPointOffsetInPixels;
                    bool fits = computeBlockPinPoint(
                        pathSegmentsLengthInPixelsOnCurrentZoom,
                        lengthOfPathInPixelsOnCurrentZoom,
                        pathSegmentsLength31,
                        path31,
                        blockWidthWithoutSpacing,
                        offsetToFirstNewBlockInPixels + blockIdx * eachNewBlockAfterFirstOffsetInPixels,
                        scanOriginPathPointIndex,
                        scanOriginPathPointOffsetInPixels,
                        nextScanOriginPathPointIndex,
                        nextScanOriginPathPointOffsetInPixels,
                        computedBlockPinPoint);
                    if (!fits)
                    {
                        assert(false);
                        break;
                    }
                    scanOriginPathPointIndex = nextScanOriginPathPointIndex;
                    scanOriginPathPointOffsetInPixels = nextScanOriginPathPointOffsetInPixels;

                    float symbolPinPointOffset = -blockWidthWithoutSpacing / 2.0f;
                    for (auto symbolIdx = 0u; symbolIdx < symbolsCount; symbolIdx++)
                    {
                        const auto& symbol = symbolsForPinPointsComputation[symbolIdx];

                        const auto offsetFromBlockPinPointInPixelOnCurrentZoom = symbolPinPointOffset + symbol.leftPaddingInPixels + symbol.widthInPixels / 2.0f;
                        const auto zoomShiftScaleFactor = qPow(2.0f, neededZoom - currentZoomLevel);
                        const auto offsetFromBlockPinPointInPixelOnNeededZoom = offsetFromBlockPinPointInPixelOnCurrentZoom * zoomShiftScaleFactor;
                        symbolPinPointOffset += symbolsFullSizesInPixels[symbolIdx];

                        ComputedPinPoint computedSymbolPinPoint;
                        fits = computeSymbolPinPoint(
                            pathSegmentsLengthInPixelsOnCurrentZoom,
                            lengthOfPathInPixelsOnCurrentZoom,
                            pathSegmentsLength31,
                            path31,
                            computedBlockPinPoint,
                            zoomShiftScaleFactor,
                            offsetFromBlockPinPointInPixelOnNeededZoom,
                            computedSymbolPinPoint);
                        if (!fits)
                        {
                            assert(false);
                            continue;
                        }
                        
                        computedPinPoints.push_back(qMove(computedSymbolPinPoint));
                    }
                }
                computedPinPointsByLayer.push_front(qMove(computedPinPoints));
            }
            else  if (numberOfSymbolsThatFit > 0)
            {
                QList<ComputedPinPoint> computedPinPoints;
                unsigned int scanOriginPathPointIndex = basePathPointIndex;
                float scanOriginPathPointOffsetInPixels = globalPaddingInPixelsFromBasePathPointOnCurrentZoom;

                // Compute base pin-point of this virtual block. Actually blocks get pinned,
                // symbols inside block just receive offset from base pin-point
                ComputedPinPoint computedBlockPinPoint;
                unsigned int nextScanOriginPathPointIndex;
                float nextScanOriginPathPointOffsetInPixels;
                bool fits = computeBlockPinPoint(
                    pathSegmentsLengthInPixelsOnCurrentZoom,
                    lengthOfPathInPixelsOnCurrentZoom,
                    pathSegmentsLength31,
                    path31,
                    fullSizeOfSymbolsThatFit,
                    offsetToFirstNewBlockInPixels,
                    scanOriginPathPointIndex,
                    scanOriginPathPointOffsetInPixels,
                    nextScanOriginPathPointIndex,
                    nextScanOriginPathPointOffsetInPixels,
                    computedBlockPinPoint);
                if (!fits)
                {
                    assert(false);
                    break;
                }
                scanOriginPathPointIndex = nextScanOriginPathPointIndex;
                scanOriginPathPointOffsetInPixels = nextScanOriginPathPointOffsetInPixels;

                float symbolPinPointOffset = -fullSizeOfSymbolsThatFit / 2.0f;
                for (auto symbolIdx = 0u; symbolIdx < numberOfSymbolsThatFit; symbolIdx++)
                {
                    const auto& symbol = symbolsForPinPointsComputation[symbolIdx];

                    const auto offsetFromBlockPinPointInPixelOnCurrentZoom = symbolPinPointOffset + symbol.leftPaddingInPixels + symbol.widthInPixels / 2.0f;
                    const auto zoomShiftScaleFactor = qPow(2.0f, neededZoom - currentZoomLevel);
                    const auto offsetFromBlockPinPointInPixelOnNeededZoom = offsetFromBlockPinPointInPixelOnCurrentZoom * zoomShiftScaleFactor;
                    symbolPinPointOffset += symbolsFullSizesInPixels[symbolIdx];

                    ComputedPinPoint computedSymbolPinPoint;
                    fits = computeSymbolPinPoint(
                        pathSegmentsLengthInPixelsOnCurrentZoom,
                        lengthOfPathInPixelsOnCurrentZoom,
                        pathSegmentsLength31,
                        path31,
                        computedBlockPinPoint,
                        zoomShiftScaleFactor,
                        offsetFromBlockPinPointInPixelOnNeededZoom,
                        computedSymbolPinPoint);
                    if (!fits)
                    {
                        assert(false);
                        continue;
                    }
                    
                    computedPinPoints.push_back(qMove(computedSymbolPinPoint));
                }
                computedPinPointsByLayer.push_back(qMove(computedPinPoints));
            }
        }

        // Move to next zoom level
        lengthOfPathInPixelsOnCurrentZoom *= 2.0f;
        for (auto& pathSegmentLengthInPixelsOnCurrentZoom : pathSegmentsLengthInPixelsOnCurrentZoom)
            pathSegmentLengthInPixelsOnCurrentZoom *= 2.0f;
        globalPaddingInPixelsFromBasePathPointOnCurrentZoom *= 2.0f;
        remainingPathLengthOnPrevZoom = remainingPathLengthOnCurrentZoom;
        if (blocksToInstantiate > 0 || totalNumberOfCompleteBlocks > 0)
        {
            if (blocksToInstantiate > 0 && totalNumberOfCompleteBlocks > 0)
            {
                // In case new blocks added and there was previous blocks, use block offset closest to start
                kOffsetToFirstBlockOnPrevZoom = qMin(kOffsetToFirstNewBlockOnCurrentZoom, kOffsetToFirstPresentBlockOnCurrentZoom);
            }
            else if (blocksToInstantiate > 0 && totalNumberOfCompleteBlocks == 0)
            {
                // In case blocks were added and they are first ones, use first new block offset
                kOffsetToFirstBlockOnPrevZoom = kOffsetToFirstNewBlockOnCurrentZoom;
            }
            else if (blocksToInstantiate == 0 && totalNumberOfCompleteBlocks > 0)
            {
                // In case no blocks were added, but there was previously blocks, use offset to first present block
                kOffsetToFirstBlockOnPrevZoom = kOffsetToFirstPresentBlockOnCurrentZoom;
            }
        }
        totalNumberOfCompleteBlocks += blocksToInstantiate;
    }

    return computedPinPointsByLayer;
}