В частях с j по m вам нужно было по-разному вычислять координаты остановок. Поэтому единственное, что должно было меняться в вашей программе — это функция вычисления координат остановок на Svg карте. В части i это были довольно простые вычисления, которые еще с части g начались. Мы здесь что делали? Мы брали все остановки и просто вычисляли линейное преобразование так, чтобы поместить эти остановки в данные ширину и высоту. В части j уже нужно было сжимать координаты. Соответственно, мы переписываем функцию вычисления координат остановок, здесь было удобно создать отдельный класс, который будет отвечать за сжатие координат. И именно этот класс в первую очередь будет преобразовываться в дальнейших частях. В конструкторе этого класса мы принимаем словарь остановок, и нас из этого словаря интересуют только координаты этих остановок, широты и долготы. Мы их складываем в контейнеры соответствующие и эти контейнеры сортируем. Нам важно видеть отсортированные широты и долготы. При этом, поскольку в реальной жизни вероятность того, что координаты остановок совпадут или даже их широты совпадут, эта вероятность очень мала, поэтому мы будем считать, что все координаты различны. Итак, мы берем, собственно, все широты и долготы, а затем считаем, что в конструкторе вся настройка произошла. И теперь вызываем отдельный метод, в котором нам нужно заполнить уже итоговые координаты всех широт и долгот на карте. Здесь уже мы вызываем метод FillTargets, в который передаем ширину, высоту и padding, то есть как бы выделяем отдельный этап сжатия координат, который уже зависит от итоговых настроек карты. И в этом методе — давайте на него отдельно посмотрим, потому что он на экран здесь не умещается — в этом методе мы делаем ровно то, что было написано в условии. Мы равномерно распределяем широты и долготы по всей ширине и, соответственно, всей высоте. Мы вычисляем, каков должен быть шаг, по сути просто деля доступную высоту на количество широт, и вычисляем такой же шаг по x. Эти шаги вычислили, теперь итерируемся по всем широтам и долготам в порядке возрастания и просто с помощью простой формулы вычисляем новые значения. Небольшой нюанс — что у нас широты и долготы хранятся не в виде вектора координат, не в виде вектора просто вещественных чисел, а в виде вектора неких структур. В этих структурах хранится два вещественных числа: это исходное значение координаты, географическое, и новое значение координаты на самой карте. И мы их просто сортируем по исходному значению, и поэтому, когда мы итерируемся, мы уже просто можем заполнять новые значения. И здесь мы в процессе поддерживаем индекс — номер этой координаты в возрастающем порядке, просто инкрементируя его от нуля и т.д. Ну и, собственно говоря, всё. Тут есть еще пара удобных методов, которые позволяют по широте получить ее новое значение и по долготе получить ее новое значение. То есть даже получился более-менее аккуратный класс с понятным публичным интерфейсом. Возвращаясь к функции вычисления координат остановок, после того, как мы вызвали метод FillTargets, мы итерируемся по всем остановкам и вычисляем новые координаты этих остановок, вызывая метод MapLon, то есть получая новую долготу, и метод MapLat, вычисляя новую широту. Это все сохраняем в словарь, и этот словарь возвращаем, остальной интерфейс не меняется. Это, собственно, решение части j. В части k нужно было добавить склейку соседних координат: склейку соседних широт и склейку соседних долгот. Мы не отказываемся от класса compressor, мы даже не отказываемся от назначения конструктора, конструктор остался, каким был. Мы не отказываемся от метода FillTargets, который делает, по сути, все финальные действия. Но при склейке у нас все-таки склеивались номера соседних координат, если вы помните. Поэтому мы добавляем дополнительный этап при сжатии координат, который выражается в методе FillIndices, то есть заполнении индексов, и, соответственно, у нас появляется дополнительная информация про координаты — сами индексы. Поскольку у нас уже есть структура CoordInfo, мы в нее добавляем индексы. и эти индексы заполняем на втором этапе сжатия координат, как раз во время склейки. Смотрим на метод FillIndices. Мы не хотим копировать код для широт и для долгот, поэтому вынесли код в отдельный приватный метод, в который передаем, собственно, отсортированный вектор широт и отсортированный вектор долгот и информацию про то, какие у нас координаты являются соседними. К этому вернемся чуть попозже, давайте пока посмотрим на интерфейс компрессора. Соответственно, мы заполняем индексы, мы заполняем их в конечном счете в приватном методе, в этом приватном методе, по сути, сделано все, что было описано в условии задачи, то есть мы перебираем все координаты, получается, по возрастанию, потому что они отсортированы. Дальше для каждой координаты нам нужно понять, можно ли ее приклеить к группе предыдущих координат. Для этого мы, по сути, перебираем все предыдущие координаты, у которых такой же индекс, и смотрим, не является ли она соседней для нашей координаты, по словарю, который называется neighbour_values. Дальше у нас в итоге получаются индексы, заполненные в структуре CoordInfo, которые используются в мало изменившемся методе FillTargets. По сути, он изменился лишь в том, что индекс при вычислении итоговой координаты не инкрементируется и вычисляется в процессе, а используется готовый из структуры CoordInfo. Заметьте, что мы ее спокойно распаковываем с помощью structured bindings и записи с помощью квадратных скобок. Ну и наконец, конечно, нам нужно было вычислить дополнительные словари вспомогательные, в которых для каждой координаты хранились соседние. Вот мы их использовали при склейке, вот здесь мы в этих словарях искали, ну и, конечно, эти словари нужно предварительно построить. Вот мы это делаем в самом начале функции ComputeStopsCoords. Это функция BuildCoordNeighboursDicts, которая строит эти самые словари просто по набору остановок и набору автобусов. В этой функции — давайте на нее отдельно посмотрим — в этой функции опять же ничего особенно интересного, вот мы заготовили словари, словарь для широт и словарь для долгот, итерируемся по всем автобусам, по всем остановкам этого автобуса и говорим, что когда мы смотрим на две соседние остановки, нам нужно сказать, что для одной из них другая — соседняя. При этом было удобно сделать так, чтобы мы только для большей остановки, у которой больше значения, соответственно, широты и долготы, хранили знания про то, что меньшая остановка является соседней. Поэтому, когда мы смотрим на две соседние остановки, у которых координаты point_prev и point_cur, мы вызываем функцию minmax, чтобы понять, у какой широта меньше, у какой широта больше, и соответственно долгота. Получаем вот эти вот числа и затем для большей широты добавляем в словарь то, что меньшая широта является соседней, и то же самое для долготы. Вот, собственно, мы эти самые словари и построили. Всё. Мы их построили. Возвращаемся к функции вычисления координат остановок, еще раз смотрим на все. Мы их передаем в метод FillIndices, эти самые словари соседей. И индексы как раз вычисляем с помощью этих самых словарей. Всё. Если у нас предыдущие остановки не являются к нам соседними, то мы можем получить тот же индекс, и эти индексы используются при вычислении итоговых координат. Ну еще, конечно, нам понадобятся вспомогательные методы вычисления максимальных индексов, которые были нужны, опять же, для того, чтобы вычислить шаг по y, шаг по x. Ну и эти методы отдельно реализованы, просто смотрим на последний элемент и берем его индекс. Всё. В части k больше ничего интересного. Изменили вычисление индексов, получили новое вычисление итоговых координат. В части l нужно было добавить предварительную интерполяцию промежуточных остановок между пересадочными по географическим координатам. Соответственно добавляется новый этап в функцию вычисления координат остановок. Называется «вычислим неинтерполированные географические координаты остановок», вот такое прекрасное название из пяти слов. И затем мы повторяем все то, что было до этого, но с использованием интерполированных координат. Понадобится небольшой рефакторинг, потому что мы теперь не можем брать исходные координаты остановок, а должны брать пересчитанные координаты остановок. Поэтому в конструктор компрессора передаются пересчитанные географические координаты, ну и, кажется, здесь больше ничего и не меняется. Сами координаты как мы будем пересчитывать? Посмотрим на функцию ComputeInterpolatedStopsGeoCoords, поднимаемся к ней. Как вы помните, мы должны были взять все конечные и пересадочные остановки и затем остальные между ними равномерно распределить. Ну, соответственно, у нас выделена функция как раз поиска опорных остановок, FindSupportStops, давайте на нее посмотрим. Это ровно то, что написано в условии задачи, FindSupportStops. Вот здесь. По сути, здесь просто как-то с помощью каких-то словарей и каких-то множеств реализован поиск конечных остановок и поиск пересадочных остановок, включая то хитрое условие, что, если автобус дважды проходит через одну и ту же остановку, то это еще не повод считать ее пересадочной, а если трижды и больше, то повод. Ну и тут есть некоторые рейтинги на эту тему, рейтинги остановок. Ну и в конечном счете набирается множество support_stops, множество опорных остановок. Вот мы его набрали, и теперь нам нужно распределить все остальные остановки между опорными. Опять же, простые вложенные циклы. Перебираем автобусы, перебираем все остановки, помним индекс последней опорной, и если текущая оказалась опорной, то все от текущей до предыдущей нужно равномерно распределить. И вот, ну, есть некоторые арифметические вычисления, которые позволяют равномерно распределить остановки по координатам. Получили новые широты, новые долготы и дальше их подставляем, просто запуская уже известный нам CoordsCompressor. Всё. Часть l просто про то, чтобы добавить дополнительный словарь с новыми геокоординатами остановок. И наконец в части m нужно было модифицировать склейку соседних широт и долгот. Поскольку вот эта самая склейка у нас уже собрана в методе FillIndices, у нас ничего не меняется в функции вычисления координат, в самой большой функции, зато меняется метод FillIndices, давайте на него снова посмотрим. Он, напомню, должен был вычислить новые номера остановок. Не 1, 2, 3, 4, 5, а 1.1.1, 2.2.2, 3.4.5, что-нибудь такое. Опять же, метод FillIndices не меняется, потому что мы общий код для широт и долгот выделили в отдельный приватный метод FillCoordIndices, и в этом приватном методе просто теперь написан чуть другой код. Другой код, который не смотрит на предыдущие остановки и ищет среди них соседнюю, а если находит, то огорчается, а наоборот: он перебирает все соседние остановки и ищет максимальный индекс среди них. Ну и по этому поводу у нас на самом деле набор соседей стал вектором, а не множеством. Ну просто потому, что если мы хотим итерироваться, но не искать, то почему бы не сделать это вектором? Вот. Такое минимальное изменение, чтобы код был чуть-чуть поэффективнее. Ну и возвращаясь к FillCoordIndices, еще раз, мы перебираем все координаты остановок от минимальной до максимальной, сначала для широт, потом для долгот, смотрим на всех соседей и для каждой соседней остановки находим ее в нашем наборе координат, находим ее индекс и говорим, что вот, у нас максимальный индекс соседней остановки — ровно то, что и нужно было сделать по условиям. Метод Find раньше был нужен только для того, чтобы в конечном счете пересчитывать координаты остановок, сейчас же он позволяет нам и какие-то промежуточные вычисления тоже реализовать. Тут есть небольшая микрооптимизация, которая на самом деле не очень нужна, которая позволяет сократить диапазон бинпоиска. Но это опять же вы можете посмотреть в опубликованном полном решении. Итак, в частях с j по m мы манипулировали координатами остановок. По сути, мы удачно выделили весь нужный код в функцию вычисления координат остановок и только эту функцию модифицировали. В самой функции мы тоже выделили какие-то логические части. Это интерполяция промежуточных остановок между опорными, это выделение соседних координат, вот этих самых словарей, это, собственно, сама склейка координат с некоторыми дополнительными этапами. Части с j по m мы обсудили.