#include <maps/wikimap/mapspro/services/mrc/libs/coord_recognition/include/coord_recognition.h>

#include <tensorflow/core/framework/graph.pb.h>
#include <tensorflow/core/framework/tensor.h>
#include <tensorflow/core/common_runtime/dma_helper.h>

#include <vector>
#include <stack>
#include <string>

namespace maps::mrc::coord_recognition {

using namespace wiki::tf_inferencer;

namespace {

const std::string TF_MODEL_RESOURCE = "/maps/mrc/coord_recognition/models/tf_model.gdef";

constexpr int TF_IMAGE_HEIGHT = 12;
constexpr int TF_IMAGE_WIDTH = 10;

struct RecognizedCoordinateValue {
    double value;
    float confidence1; // увереность, что это первая координата на строке
    float confidence2; // увереность, что это вторая координата на строке
};

struct SymbolInfo {
    enum SymbolType {
        None    = 0,
        Single  = 1,
        Multi   = 2,
    } type;
    int batchIdxFrom;
    int batchSize;
    std::vector<cv::Range> internalSegments;
    int symbolPixelsSize;
};

cv::Mat brightnessColummn(const cv::Mat& gray) {
    cv::Mat column;
    cv::reduce(gray, column, 1, cv::REDUCE_SUM, CV_32FC1);
    return column / gray.cols / 255.f;
}

cv::Mat brightnessRow(const cv::Mat& gray) {
    cv::Mat row;
    cv::reduce(gray, row, 0, cv::REDUCE_SUM, CV_32FC1);
    return row / gray.rows / 255.f;
}

cv::Mat expandImage(const cv::Mat& gray)
{
    REQUIRE(0 < gray.size().area(), "Image is empty");
    if (gray.cols == TF_IMAGE_WIDTH)
        return gray;
    cv::Mat result(TF_IMAGE_HEIGHT, TF_IMAGE_WIDTH, CV_8UC1);
    result.setTo(255.);
    const int fromx = (TF_IMAGE_WIDTH - gray.cols) / 2;
    const int tox = fromx + gray.cols;
    gray.copyTo(result.colRange(fromx, tox));
    return result;
}

int panelHeight(
    const cv::Mat& brightnessColumn,
    int minLineHeight)
{
    constexpr float threshold = 0.2f;

    cv::Mat diff =
        brightnessColumn.rowRange(0, brightnessColumn.rows - 1)
        - brightnessColumn.rowRange(1, brightnessColumn.rows);

    for (int row = minLineHeight; row < diff.rows; row++) {
        if (threshold < diff.at<float>(row, 0))
            return row;
    }
    return brightnessColumn.rows + 1;
}

std::vector<cv::Range> extractTextLinesRanges(
    const cv::Mat& diff,
    int minLineHeight,
    int maxLineHeight,
    double threshold)
{
    std::vector<int> starts;
    std::vector<int> ends;
    for (int row = 0; row < diff.rows; row++) {
        const float val = diff.at<float>(row, 0);
        if (val < -threshold)
            starts.emplace_back(row);
        else if (val > threshold)
            ends.emplace_back(row);
    }
    std::vector<std::pair<cv::Range, double>> linesPurpose;
    for (size_t startIdx = 0; startIdx < starts.size(); startIdx++) {
        for (size_t endIdx = 0; endIdx < ends.size(); endIdx++) {
            if (minLineHeight < ends[endIdx] - starts[startIdx] &&
                ends[endIdx] - starts[startIdx] < maxLineHeight) {
                cv::Range range(starts[startIdx], ends[endIdx] + 1);
                const double confidence
                    = abs(diff.at<float>(starts[startIdx], 0)) * abs(diff.at<float>(ends[endIdx], 0));
                linesPurpose.emplace_back(range, confidence);
            }
        }
    }
    if (linesPurpose.empty())
        return{};
    const int ymin = starts[0];
    std::vector<std::pair<std::vector<cv::Range>, double>> linesPackPurpose;
    for (size_t i = 0; i < linesPurpose.size(); i++) {
        const cv::Range& first = linesPurpose[i].first;
        bool canSecond = false;
        for (size_t j = i + 1; j < linesPurpose.size(); j++) {
            const cv::Range& second = linesPurpose[j].first;
            const int maxSize = std::max(first.size(), second.size());
            const int minSize = std::min(first.size(), second.size());
            if (first.end < second.start && 2 * maxSize <= 3 * minSize) {
                canSecond = true;
                std::vector<cv::Range> pack;
                pack.push_back(first);
                pack.push_back(second);
                const double confidence =
                    linesPurpose[i].second * linesPurpose[j].second
                    * 1. / ((double)(first.start - ymin) * (double)(first.start - ymin) + 1.)
                    * ((double)first.size() + (double)second.size()) /  (abs((double)first.size() - (double)second.size()) + 1.) / 2.;
                linesPackPurpose.push_back({ pack, confidence });
            }
        }

        const double confidenceSingle =
            linesPurpose[i].second * linesPurpose[i].second
            * 1. / ((double)(first.start - ymin) * (double)(first.start - ymin) + 1.)
            * (canSecond ? 0.2 : 1.0);
        linesPackPurpose.push_back({ { first }, confidenceSingle });
    }

    std::sort(linesPackPurpose.begin(), linesPackPurpose.end(),
        [](const std::pair<std::vector<cv::Range>, double>& a, const std::pair<std::vector<cv::Range>, double>& b) {return a.second > b.second;}
    );
    return linesPackPurpose[0].first;
}

std::vector<cv::Mat> extractTextLines(
    const cv::Mat& gray,
    int maxPanelHeight = 20,
    double maxPanelHeightFrac = 0.08,
    int minLineHeight = 7,
    int maxLineHeight = 25,
    double threshold = 0.24,
    int expandLineHeightDelta = 2)
{

    if (maxPanelHeight < maxPanelHeightFrac * gray.rows)
        maxPanelHeight = int(maxPanelHeightFrac * gray.rows);

    cv::Mat temp = brightnessColummn(gray.rowRange(0, maxPanelHeight));
    cv::Mat column;
    temp.rowRange(0, panelHeight(temp, minLineHeight) - 1).copyTo(column);
    cv::normalize(column, column, 0., 1., cv::NORM_MINMAX);
    cv::Mat diff = column.rowRange(1, column.rows) - column.rowRange(0, column.rows - 1);

    std::vector<cv::Range> lines =
        extractTextLinesRanges(
            diff,
            minLineHeight,
            maxLineHeight,
            threshold
        );
    if (lines.empty())
        return{};

    //expand lines
    int last = 0;
    for (size_t i = 0; i < lines.size(); i++) {
        int halfDelta = std::min(expandLineHeightDelta, std::max(0, (lines[i].start - last) / 2));
        lines[i].start -= halfDelta;
        if (i == lines.size() - 1)
            break;
        halfDelta = std::min(expandLineHeightDelta, std::max(0, (lines[i + 1].start - lines[i].end) / 2));
        lines[i].end += halfDelta;
        last = lines[i].end;
    }
    lines.back().end += expandLineHeightDelta;

    std::vector<cv::Mat> result;
    for (size_t i = 0; i < lines.size(); i++) {
        result.push_back(gray.rowRange(lines[i]));
    }
    return result;
}

int textLineWidth(const cv::Mat& line)
{
    cv::Mat row = brightnessRow(line);
    const int whiteWidth = int(row.cols * .1);
    const float mean = (float)cv::norm(row.colRange(row.cols - whiteWidth, row.cols), cv::NORM_L1) / (float)whiteWidth;
    const float div = (float)cv::norm(row.colRange(row.cols - whiteWidth, row.cols) - mean, cv::NORM_L2);
    int textCols = row.cols - whiteWidth - 1;
    for (; 0 <= textCols; textCols--) {
        const float dist = abs(row.at<float>(0, textCols) - mean);
        if (dist > div)
            return textCols;
    }
    return row.cols;
}

int calculateMedianWidth(const std::vector<cv::Range> &segments)
{
    std::vector<int> segSize(segments.size());
    for (size_t i = 0; i < segments.size(); i++) {
        REQUIRE(0 < segments[i].size(), "Segments size equal to zero");
        segSize[i] = segments[i].size();
    }
    size_t n = segSize.size() / 2;
    std::nth_element(segSize.begin(), segSize.begin() + n, segSize.end());
    return segSize[n];
}

std::vector<int> splitHorzByConnectedPoints(const cv::Mat &binImage)
{
    std::stack<cv::Point2i> points;
    cv::Mat mask = cv::Mat::zeros(binImage.size(), CV_8UC1);

    int col = 0;
    std::vector<int> result;
    for (;col < binImage.cols; col++)
    {
        for (int row = 0; row < binImage.rows; row++)
        {
            if (0 == binImage.at<uchar>(row, col)) {
                points.emplace(col, row);
            }
        }
        if (points.empty())
            continue;
        while (!points.empty())
        {
            cv::Point2i pt = points.top();
            points.pop();
            if (mask.at<uchar>(pt) != 0)
                continue;
            mask.at<uchar>(pt) = 1;
            col = std::max(col, pt.x);
            if (col == binImage.cols - 1)
                break;
            if (0 < pt.y)
            {
                if (0 == binImage.at<uchar>(pt.y - 1, pt.x))
                    points.emplace(pt.x, pt.y - 1);
                if (0 == binImage.at<uchar>(pt.y - 1, pt.x + 1))
                    points.emplace(pt.x + 1, pt.y - 1);
            }
            if (0 == binImage.at<uchar>(pt.y, pt.x + 1))
                points.emplace(pt.x + 1, pt.y);
            if (pt.y < binImage.rows - 1)
            {
                if (0 == binImage.at<uchar>(pt.y + 1, pt.x))
                    points.emplace(pt.x, pt.y + 1);
                if (0 == binImage.at<uchar>(pt.y + 1, pt.x + 1))
                    points.emplace(pt.x + 1, pt.y + 1);
            }
        }
        result.push_back(col + 1);
    }
    return result;
}

std::vector<cv::Range> extractSymbols(const cv::Mat& line, int minSymbolWidth = 1)
{
    cv::Mat lineBin;
    cv::threshold(line, lineBin, 160, 255, cv::THRESH_BINARY);
    cv::Mat row;
    cv::reduce(lineBin, row, 0, cv::REDUCE_SUM, CV_32SC1);

    const int whiteColVal = 255 * lineBin.rows;
    std::vector<cv::Range> segments;
    int start = 0;
    for (int col = 0; col < row.cols; col++) {
        int val = row.at<int>(0, col);
        if (start <= 0) {
            if (val == whiteColVal)
                continue;
            start = col;
        } else {
            if (val < whiteColVal)
                continue;
            if (col - start >= minSymbolWidth)
                segments.emplace_back(start, col);
            start = -1;
        }
    }
    if (0 < start && row.cols - 1 - start >= minSymbolWidth)
        segments.emplace_back(start, row.cols - 1);

    const int medianWidth = calculateMedianWidth(segments);

    // разрезаем сегменты шире двух третей медианы на несвязанные (8-связь) сегменты
    for (size_t i = 0; i < segments.size(); i++) {
        const cv::Range& segment = segments[i];
        if (3 * medianWidth >= 2 * segment.size())
            continue;
        std::vector<int> horzSpliter = splitHorzByConnectedPoints(lineBin.colRange(segment));
        if (1 >= horzSpliter.size())
            continue;
        const int offset = segment.start;
        segments[i].end = horzSpliter[0] + offset;
        for (int j = 0; j < (int)horzSpliter.size() - 1; j++) {
            segments.insert(
                segments.begin() + i + j + 1,
                cv::Range(horzSpliter[j] + offset, horzSpliter[j + 1] + offset));
        }
        i += (horzSpliter.size() - 1);
    }
    return segments;
}

RecognizedLine extractSymbolsExt(
    const std::string& symbols,
    const std::vector<float>& confidences,
    const std::vector<cv::Range>& internalSegments,
    int iWidth)
{
    constexpr float CONF_THRESHOLD = 0.5f;
    constexpr char NEGATIVE_SYMBOL = ' ';

    std::vector<std::pair<int, float>> idxAndConfidences;
    for (int i = 0; i < (int)confidences.size(); i++) {
        idxAndConfidences.emplace_back(i, confidences[i]);
    }
    std::sort(
        idxAndConfidences.begin(),
        idxAndConfidences.end(),
        [](const std::pair<int, float>& a, const std::pair<int, float>& b) {
        return a.second > b.second;
    });

    std::vector<int> charsIdx(iWidth, -1);
    for (size_t i = 0; i < idxAndConfidences.size(); i++) {
        if (idxAndConfidences[i].second < CONF_THRESHOLD)
            break;
        const int idx = idxAndConfidences[i].first;
        if (symbols[idx] == NEGATIVE_SYMBOL) {
            // negative
            continue;
        }
        const cv::Range& range = internalSegments[idx];
        cv::Range maxValid(-1, -1);
        cv::Range valid(range.start, range.start);
        for (int r = range.start; r < range.end; r++) {
            if (-1 == charsIdx[r]) {
                valid.end = r + 1;
                continue;
            }
            if (maxValid.size() < valid.size()) {
                maxValid = valid;
            }
            valid.start = valid.end = r;
        }
        if (maxValid.size() < valid.size()) {
            maxValid = valid;
        }
        if (maxValid.size() > range.size() / 2) {
            std::fill(
                charsIdx.begin() + maxValid.start,
                charsIdx.begin() + maxValid.end,
                idx);
        }
    }

    RecognizedLine result;
    int lastIdx = -1;
    for (size_t i = 0; i < charsIdx.size(); i++) {
        if (charsIdx[i] == lastIdx)
            continue;
        lastIdx = charsIdx[i];
        if (charsIdx[i] == -1)
            continue;
        result.str += symbols[charsIdx[i]];
        result.confidences.push_back(confidences[charsIdx[i]]);
    }
    return result;
}

void pickAllSymbolPositions(const std::string& str, char ch, std::vector<int>& positions)
{
    constexpr int POSITION_OFFSET = 2;
    if (str.size() < 2 * POSITION_OFFSET)
        return;
    const char* begin = str.c_str();
    const char* ptr = begin + POSITION_OFFSET;
    const char* end = begin + str.size() - POSITION_OFFSET;
    for (; ptr != end; ptr++) {
        if (ch == *ptr)
            positions.push_back((int)(ptr - begin));
    }
}

RecognizedCoordinateValue extractCoordinate(const std::string& textLine, int degreeIdx)
{
    int idx = degreeIdx - 1;
    for (;0 <= idx; idx--) {
        if (!std::isdigit(textLine[idx]))
            break;
    }
    if (idx == degreeIdx - 1)
        return {-1., 0., 0.};

    RecognizedCoordinateValue result = {-1., 1., 1.};
    if (0 == idx) {
        result.confidence1 *= 0.9;
        result.confidence2 *= 0.9;
    } else {
        if (textLine[idx] != '/')
            result.confidence1 *= 0.9;
        if (textLine[idx] != '.')
            result.confidence2 *= 0.9;
    }
    const int degree = std::stoi(textLine.substr(idx + 1, degreeIdx - idx - 1));

    idx = degreeIdx + 1;
    int dotIdx = -1;
    for (;idx < (int)textLine.size(); idx++) {
        if (!std::isdigit(textLine[idx])) {
            if ((-1 != dotIdx) || (degreeIdx + 1 == idx))
                break;
            if ('.' == textLine[idx]) {
                dotIdx = idx;
            } else if (' ' == textLine[idx]) { // some times dot didn't recognize
                dotIdx = idx;
                result.confidence1 *= 0.9;
                result.confidence2 *= 0.9;
            } else {
                break;
            }
        }
    }
    if (idx >= (int)textLine.size()) {
        result.confidence1 = 0.0;
        result.confidence2 *= 0.9;
    } else if (idx == degreeIdx + 1) {
        return{ -1., 0., 0. };
    } else {
        if (degreeIdx + 3 != dotIdx) {
            result.confidence1 *= 0.9;
        }
        if (degreeIdx + 4 != dotIdx && degreeIdx + 3 != dotIdx) {
            result.confidence2 *= 0.9;
        }
        if (textLine[idx] != '\'') {
            result.confidence1 *= 0.9;
            result.confidence2 *= 0.9;
        }
    }
    std::string minutesStr = textLine.substr(degreeIdx + 1, idx - degreeIdx - 1);
    if (-1 != dotIdx)
        minutesStr[dotIdx - degreeIdx - 1] = '.';
    const double minutes = std::stof(minutesStr);
    result.value = degree + minutes / 60.;
    return result;
}

Coordinates extractCoordinates(const RecognizedLine &line)
{
    constexpr double PROBABILITY_NOT_DEGREE = 0.95;

    Coordinates result = {-1., -1., 0.0};

    std::vector<int> degreeIdxs;
    pickAllSymbolPositions(line.str, '*', degreeIdxs);
    // some times classifier confused between minute sign and degree sign
    pickAllSymbolPositions(line.str, '\'', degreeIdxs);
    if (degreeIdxs.size() < 2)
        return result;

    std::sort(degreeIdxs.begin(), degreeIdxs.end());

    std::vector<RecognizedCoordinateValue> candidates;
    for (size_t i = 0; i < degreeIdxs.size(); i++) {
        const int idx = degreeIdxs[i];
        RecognizedCoordinateValue coordValue = extractCoordinate(line.str, idx);
        if (line.str[idx] != '*') {
            coordValue.confidence1 *= PROBABILITY_NOT_DEGREE;
            coordValue.confidence2 *= PROBABILITY_NOT_DEGREE;
        }
        if (0. < coordValue.confidence1 ||
            0. < coordValue.confidence2)
        {
            candidates.push_back(coordValue);
        }
    }
    if (candidates.size() < 2)
        return result;
    if (candidates.size() == 2)
    {
        result.first = candidates[0].value;
        result.second = candidates[1].value;
        result.confidence = candidates[0].confidence1 * candidates[1].confidence2;
        return result;
    }

    for (int i = 0; i < (int)candidates.size() - 1; i++) {
        for (int j = i + 1; j < (int)candidates.size(); j++) {
            const float conf = candidates[i].confidence1 * candidates[j].confidence2;
            if (result.confidence < conf) {
                result.first  = candidates[i].value;
                result.second = candidates[j].value;
                result.confidence = conf;
            }
        }
    }
    return result;
}
} // namespace

CoordRecognition::CoordRecognition()
    : tfInferencer_(TensorFlowInferencer::fromResource(TF_MODEL_RESOURCE))
{
    evalSupportedSymbols();
}

void CoordRecognition::evalSupportedSymbols()
{
    const std::string TF_CLASS_NAMES_LAYER_NAME = "class_names:0";

    std::vector<TString> strs = tensorToVector<TString>(
        tfInferencer_.inference(TF_CLASS_NAMES_LAYER_NAME)
        );

    for (const auto& str : strs) {
        REQUIRE(str.size() == 1, "Classes name must have one symbol only");
        symbols_ += str[0];
    }
}

RecognizedLine CoordRecognition::recognizeSymbols(const ImagesBatch &imagesBatch) const
{
    const std::string TF_INPUT_LAYER_NAME   = "image_input";
    const std::string TF_SOFTMAX_LAYER_NAME = "softmax_results:0";
    std::vector<tensorflow::Tensor> resultTensors = tfInferencer_.inference(
        TF_INPUT_LAYER_NAME,
        imagesBatch,
        { TF_SOFTMAX_LAYER_NAME});

    REQUIRE(1 == resultTensors.size(), "Invalid output tensors number");
    REQUIRE((2 == resultTensors[0].dims()) && ((int)imagesBatch.size() == resultTensors[0].dim_size(0)),
        "Invalid output tensor dimension");
    REQUIRE((int)symbols_.size() == resultTensors[0].dim_size(1),
        "Output tensor has dimension different from valid symbols count");

    const float *pConfidence = static_cast<const float*>(tensorflow::DMAHelper::base(&resultTensors[0]));
    const int validSymbols = resultTensors[0].dim_size(1);
    RecognizedLine result;
    for (size_t symbIdx = 0; symbIdx < imagesBatch.size(); symbIdx++) {
        char symbol = symbols_[0];
        float confidence = pConfidence[0];
        for (int i = 1; i < validSymbols; i++) {
            if (confidence < pConfidence[i]) {
                symbol = symbols_[i];
                confidence = pConfidence[i];
            }
        }
        result.str += symbol;
        result.confidences.push_back(confidence);
        pConfidence += validSymbols;
    }
    return result;
}

RecognizedLine CoordRecognition::recognizeTextLine(const cv::Mat& grayLine) const
{
    constexpr int SYMBOL_WIDTH          = 8;
    constexpr int MULTISYMBOL_STEP      = 1;
    constexpr int MULTISYMBOL_MINSIZE   = 4;

    cv::Mat partLine = grayLine.colRange(0, textLineWidth(grayLine));

    std::vector<cv::Range> symbolsRange = extractSymbols(partLine);
    if (0 == symbolsRange.size())
        return {"", {}};

    ImagesBatch batch;
    std::vector<SymbolInfo> symbolsInfo;
    for (size_t idxSymb = 0; idxSymb < symbolsRange.size(); idxSymb++)
    {
        const cv::Range& range = symbolsRange[idxSymb];
        const int newWidth = TF_IMAGE_HEIGHT * range.size() / partLine.rows;
        if (0 == newWidth) {
            SymbolInfo info;
            info.type = SymbolInfo::SymbolType::None;
            info.batchIdxFrom = -1;
            info.batchSize = 0;
            info.symbolPixelsSize = 0;

            symbolsInfo.emplace_back(info);
        } else if (newWidth < SYMBOL_WIDTH) {
            cv::Mat singleSymbImage;
            cv::resize(partLine.colRange(range), singleSymbImage, cv::Size(newWidth, TF_IMAGE_HEIGHT));

            SymbolInfo info;
            info.type = SymbolInfo::SymbolType::Single;
            info.batchIdxFrom = batch.size();
            info.batchSize = 1;
            info.symbolPixelsSize = newWidth;

            batch.push_back(expandImage(singleSymbImage));
            symbolsInfo.emplace_back(info);
        } else {
            cv::Mat multiSymbImage;
            cv::resize(partLine.colRange(range), multiSymbImage, cv::Size(newWidth, TF_IMAGE_HEIGHT));

            SymbolInfo info;
            info.type = SymbolInfo::SymbolType::Multi;
            info.batchIdxFrom = batch.size();
            info.batchSize = 0;
            info.symbolPixelsSize = newWidth;

            for (int i = 0; i < multiSymbImage.cols - MULTISYMBOL_MINSIZE; i += MULTISYMBOL_STEP) {
                for (int size = MULTISYMBOL_MINSIZE;
                         size < std::min(TF_IMAGE_WIDTH + 1, multiSymbImage.cols - i + 1);
                         size += 1)
                {
                    const cv::Range newRange(i, i + size);
                    info.batchSize++;
                    info.internalSegments.push_back(newRange);
                    batch.push_back(expandImage(multiSymbImage.colRange(newRange)));
                }
            }
            symbolsInfo.emplace_back(info);
        }
    }
    if (0 == batch.size())
        return{ "",{} };

    RecognizedLine recognized = recognizeSymbols(batch);

    RecognizedLine result;
    for (size_t i = 0; i < symbolsInfo.size(); i++) {
        const SymbolInfo& symbolInfo = symbolsInfo[i];
        if (symbolInfo.type == SymbolInfo::SymbolType::None) {
            result.str += " ";
            result.confidences.push_back(0.f);
        } else if (symbolInfo.type == SymbolInfo::SymbolType::Single) {
            result.str += recognized.str[symbolInfo.batchIdxFrom];
            result.confidences.push_back(recognized.confidences[symbolInfo.batchIdxFrom]);
        } else {
            RecognizedLine recognizedExt =
                extractSymbolsExt(
                    recognized.str.substr(symbolInfo.batchIdxFrom, symbolInfo.batchSize),
                    {
                        recognized.confidences.begin() + symbolInfo.batchIdxFrom,
                        recognized.confidences.begin() + symbolInfo.batchIdxFrom + symbolInfo.batchSize
                    },
                    symbolInfo.internalSegments, symbolInfo.symbolPixelsSize);
            result.str += recognizedExt.str;
            result.confidences.insert(
                result.confidences.end(),
                recognizedExt.confidences.begin(),
                recognizedExt.confidences.end());
        }
    }
    return result;
}

Coordinates CoordRecognition::recognize(const cv::Mat &image) const
{
    const std::string TF_INPUT_LAYER_NAME = "image_input";
    const std::string TF_SOFTMAX_LAYER_NAME = "softmax_results";

    Coordinates result = { -1.0, -1.0, 0.0 };

    cv::Mat gray;
    cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
    std::vector<cv::Mat> textLines = extractTextLines(gray);
    for (size_t i = 0; i < textLines.size(); i++) {
        const cv::Mat& line = textLines[i];
        RecognizedLine recognizedLine = recognizeTextLine(line);
        Coordinates coord = extractCoordinates(recognizedLine);
        if (coord.confidence > result.confidence)
            result = coord;
    }
    return result;
}

} // maps::mrc::coord_recognition

