惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recorded Future
Recorded Future
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Scott Helme
Scott Helme
Cyberwarzone
Cyberwarzone
C
CERT Recently Published Vulnerability Notes
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
Google Online Security Blog
Google Online Security Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
WordPress大学
WordPress大学
博客园 - 聂微东
L
LINUX DO - 最新话题
月光博客
月光博客
小众软件
小众软件
T
Troy Hunt's Blog
A
Arctic Wolf
量子位
I
Intezer
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
Schneier on Security
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
美团技术团队
The Cloudflare Blog
P
Privacy International News Feed
S
Security Affairs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
GRAHAM CLULEY
N
News | PayPal Newsroom
Apple Machine Learning Research
Apple Machine Learning Research
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 热门话题
V
Vulnerabilities – Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
宝玉的分享
宝玉的分享
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
Hacker News: Ask HN
Hacker News: Ask HN
雷峰网
雷峰网

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Тап по тысяче точек за O(log n): QuadTree и сферическая геометрия в гео-соцсети
Daniil Nuzhdin · 2026-06-28 · via Все публикации подряд на Хабре

9 лет назад я разрабатывал геолокационную соц.сеть на заказ, где мы отображали чаты на карте. До релиза не дошло, но интересного опыта было получено много. В очередной статье из серии рассказываю про то, как обрабатывать нажатия на MapBox и любой другой карте (Google, Yandex – не важно) и находить нужные объекты на ней, привязанные к координатам.

В прошлой части я рассказывал, как я отказался от UIView-аннотаций на карте и перенёс отрисовку облаков на GL-слои. С производительностью я тогда выиграл: FPS вырос, карта перестала тормозить от тысяч тестовых UIView, скролл и зум наконец-то стали плавными.

Но у этого решения обнаружилась неприятная обратная сторона.

Пока облака оставались обычными аннотациями, Mapbox «из коробки» понимал, куда именно нажал пользователь. С UIView работают методы hitTest и pointInside, соответственно. нажал – получил view от UIKit, а к ней привязанный объект

Как только облако превратилось в обычную картинку внутри MapBox-слоя, для карты оно перестало существовать как интерактивный элемент, так что теперь при тапе MapBox выдает только координату точки касания.

Параллельно я получил еще одну задачу: нужно было определять, под какими именно облаками в текущей видимой области стоит рисовать «ауру». Включать её для вообще всех точек — плохая идея, карта быстро превратится в кашу из свечения. Соответственно, надо было решать следующие задачи:

  1. Какие облака находятся в радиусе тапа

  2. Какие объекты попали в текущий экран

  3. Что находится сразу за границами экрана, чтобы при сдвиге карты элементы не появлялись из ниоткуда

По сути, всё это упирается в пространственный поиск.

Вариант «в лоб»: перебираем массив

Самое очевидное решение для обработки тапа выглядит так:

func cloud(at tap: CLLocationCoordinate2D) -> Cloud? {
    return allClouds
        .filter { $0.coordinate.distance(to: tap) < someRadius }
        .min { $0.coordinate.distance(to: tap) < $1.coordinate.distance(to: tap) }
}

Когда на карте живет сотня объектов, это работает. Пробежались по массиву, посчитали дистанцию, нашли минимум. Проблем нет, всё работает быстро.

Проблемы начинаются, когда база разрастается до нескольких тысяч точек, а пользователь активно двигает карту, меняет масштаб и спамит тапами. Линейный перебор за O(n) можно делать только в фоне, но пользователь будет получать всё равно результат с большой задержкой, даже если ничего не будет тормозить.

К тому же расстояние между точками CLLocationCoordinate2D — это не просто школьная формула sqrt(dx² + dy²). Наша планета — сфера, так что приходится возиться с тригонометрией. На единичных операциях это незаметно, но если гонять математические операции тысячу раз на каждое движение пользователем карты, упремся в потерю производительности, а юзер наш будет грустить :)

Стало понятно, что перебор — тупиковый путь. Нужен был пространственный индекс, позволяющий отсекать заведомо далекие участки мира. Для работы с двумерными координатами удобно использовать QuadTree.

Как устроен QuadTree

Принцип работы QuadTree довольно простой: берем один большой прямоугольник, описывающий весь мир, и начинаем складывать туда точки. Пока их мало, они лежат прямо в этом корневом узле. Как только количество точек превышает лимит, мы делим этот прямоугольник на четыре меньшие части — прямоугольные квадранты — и распределяем элементы по дочерним узлам.

В итоге мы получаем дерево, где каждый узел контролирует свою гео-зону. Когда нам нужно найти точки в конкретной области, мы обходим только те ветки, которые пересекаются с нашим запросом. Остальные отбрасываем сразу.

Я выставил лимит в 10 точек на один узел:

private let MaxItemsInSquare = 10

func add(item: QuadItem) -> Node {
    // Если у узла уже есть дети — спускаемся глубже
    if !emptyChildren {
        return addItemToChild(item)
    }

    // Если лимит не превышен или прямоугольник стал слишком маленьким — сохраняем здесь
    if items.count < MaxItemsInSquare || rect.width < 10 || rect.height < 10 {
        items.append(item)
        return self
    }

    // Лимит превышен: бьем узел на 4 части и переносим старые точки вниз
    for existing in items {
        addItemToChild(existing)
    }

    addItemToChild(item)
    items.removeAll()

    return self
}

Определить нужный квадрант для точки — дело двух проверок: выясняем, левее она или правее центра, и выше или ниже.

func quad(in rect: CGRect, for point: CGPoint) -> Int {
    let x = point.x > rect.midX ? 1 : 0
    let y = point.y > rect.midY ? 1 : 0

    return 2 * y + x
}

Для поиска объектов в заданной области рекурсивно обходим дерево:

func findItems(in rect: CGRect, exceptRect: CGRect = .zero) -> [QuadItem]? {
    if exceptRect.contains(self.rect) {
        return nil
    }

    if emptyChildren {
        guard self.rect.intersects(rect) else {
            return nil
        }

        return items.filter {
            rect.contains($0.mapPoint) && !exceptRect.contains($0.mapPoint)
        }
    }

    return children
        .compactMap { $0?.findItems(in: rect, exceptRect: exceptRect) }
        .flatMap { $0 }
}

Обратите внимание на параметр exceptRect. Он нужен для исключения определенной зоны из результатов поиска. Позже объясню, как это помогло нмнем при расчете объектов за пределами экрана. QuadTree хорошо тем, что в наиболее частом случае поиск у нас за O(log n). Если все точки на карте окажутся на одном стадионе, мы, конечно, получим O(n), так как получим просто связный список вместо дерева. Но при более-менее равномерном распределении объектов QuadTree спасает от необходимости перебирать тысячи точек, сужая выборку до нескольких десятков кандидатов.

Почему нельзя строить дерево на широте и долготе

Первое, что приходит в голову – закинуть в дерево напрямую latitude и longitude, но Земля не плоская, один градус долготы на экваторе по протяженности в метрах сильно отличается от градуса долготы где-нибудь под Мурманском. Если резать такую координатную сетку пополам, математика внутри дерева начнет разъезжаться, а радиусы тапов на карте будут визуально деформироваться в зависимости от удаления от экватора.

Чтобы избежать этого, мы переводим координаты в проекцию сферического Меркатора. Это та самая система, в которой MapBox и другие провайдеры карт рендерят карту.

func add(item: QuadItem) {
    item.mapPoint = projection.point(for: item.coordinate)
    ...
}

В дерево уходит обычный CGPoint. Весь мир для индекса превращается в стабильный прямоугольник — в частном случае квадрат — размером mWorldWidth × mWorldWidth. Его можно безболезненно делить, пересекать с экраном и использовать для расчетов. Сами CLLocationCoordinate2D мы сохраняем внутри объектов — они еще понадобятся для финального вычисления точных расстояний.

Обработка тапа: пиксели, проекции и кривизна Земли

Задача звучит просто: пользователь тапает по экрану, и нам нужно найти облака в радиусе, скажем, 25 пикселей от пальца. Именно пикселей, а не метров, чтобы чувствительность интерфейса не менялась при изменении масштаба.

Но под капотом данные постоянно конвертируются:

  1. Точка нажатия приходит в CLLocationCoordinate2D.

  2. Индекс ищет по CGPoint в Меркаторе.

  3. Радиус задан в экранных пикселях.

  4. Физический размер пикселя в метрах плавает в зависимости от текущего зума и широты.

Алгоритм сбора данных получился следующим:

func items(at location: CLLocationCoordinate2D) -> [Cloud]? {
    let point = quadTree.point(for: location)

    // Выясняем, сколько метров в пикселе на данной широте
    let mpp = CloodsUtils.metersPerPixel(at: location.latitude, zoom: currentZoom)
    let maxMeters = 25 * mpp

    // Сдвигаем координату на нужное расстояние в метрах
    let offset = SphericalUtil.computeOffset(from: location, distance: maxMeters, heading: 0)
    let offsetPoint = quadTree.point(for: offset)

    // Считаем радиус в единицах проекции QuadTree
    let r = max(abs(offsetPoint.y - point.y), abs(offsetPoint.x - point.x))

    let searchRect = CGRect(x: point.x - r, y: point.y - r, width: 2 * r, height: 2 * r)

    // Запрашиваем кандидатов у дерева и сортируем по реальной дистанции
    return quadTree.findItems(in: searchRect)?
        .sorted {
            $0.coordinate.distance(to: location) < $1.coordinate.distance(to: location)
        }
}

Здесь есть компромисс: QuadTree ищет объекты не в круге, а в прямоугольной — квадратной — области вокруг точки тапа. Так банально быстрее. И уже эту отфильтрованную «кучку» кандидатов мы сортируем по точной тригонометрической формуле расстояния. Поскольку на этом этапе точек остается всего ничего, тяжелая геометрия не бьет по перформансу.

Зачем нужен computeOffset, если можно просто прибавить дельту к широте?

Проблема тут, что на одном зуме радиус кнопки кажется нормальным, на другом — схлопывается, а ближе к полюсам область клика начинает вести себя непредсказуемо., поэтому для смещения координат используется честный computeOffset. Код адаптирован из библиотеки Google Maps Android Utils. Он рассчитывает положение точки на сфере, зная исходную позицию, расстояние и азимут:

static func computeOffset(
    from: CLLocationCoordinate2D,
    distance: Double,
    heading: Double
) -> CLLocationCoordinate2D {
    let d = distance / EARTH_RADIUS
    let h = toRadians(heading)
    let fromLat = toRadians(from.latitude)

    let sinLat = cos(d) * sin(fromLat) + sin(d) * cos(fromLat) * cos(h)

    let dLng = atan2(
        sin(d) * cos(fromLat) * sin(h),
        cos(d) - sin(fromLat) * sinLat
    )

    return CLLocationCoordinate2DMake(
        toDegrees(asin(sinLat)),
        toDegrees(toRadians(from.longitude) + dLng)
    )
}

Этот расчет точен, но прогонять его через весь массив данных на каждый сдвиг карты слишком дорого. QuadTree как раз и решает эту проблему: мы используем “дорогую” математику только на узкой выборке объектов.

Фильтрация данных: боремся с визуальным шумом

Когда мы научились быстро собирать точки в текущей области видимости, возникла продуктовая задача. Если в видимую область попадают сотни облаков, подсвечивать их все нельзя – экран превратится в сплошное яркое пятно.

Первая мысль – отбирать топ-N по весу, то есть показывать только самые популярные или активные. Идея хороша в мире, где “богатые богатеют, а бедные беднеют”, так как чем больше активность у чата, тем больше шансов ему быть замеченным. При нескольких активных чатах все неактивные тут же теряются на их фоне.

Карта должна ощущаться «живой». Пользователю важен общий контекст активности в пространстве. Поэтому мы перешли на пропорциональную схему отбора объектов 20/60/20: берем немного самых тяжелых, чуть-чуть из середины и плотную пачку мелких точек, чтобы дать им тоже шанс выйти в топ.

items.sort { $0.weight > $1.weight }

let big = Int((0.2 * Double(maxCount)).rounded())
let middle = Int((0.2 * Double(maxCount)).rounded())
let small = maxCount - big - middle

// Самые активные
for i in 0..<big {
    result.append(items[i])
}

// Самые тихие
for i in 0..<small {
    result.append(items[items.count - 1 - i])
}

// Из середины списка
let offset = (items.count - big - middle) / 2 + big
for i in 0..<middle {
    result.append(items[offset + i])
}

Мы намеренно отдали приоритет middle-сегменту, так как за счет этого они равномерно распределяются по площади экрана, создавая ощущение заполненности мира, вместо того чтобы стягивать все внимание в пару ярких кластеров.

Работа на опережение: кэш за границами экрана

Последний штрих касается плавности скролла. Если рассчитывать эффекты только строго под размеры экрана, то при сдвиге карты новые облака будут влетать в кадр слишком резко.

Чтобы сгладить этот эффект, мы делим область поиска на два контура:

let inside = topClouds(in: visibleBounds, max: 15)
let outside = topClouds(in: extendedBounds, except: visibleBounds, max: 100)
  • visibleBounds — то, что видит пользователь прямо сейчас.

  • extendedBounds — буферная зона вокруг экрана.

Для буферной зоны как раз и пригодился параметр exceptRect. Мы просим дерево: “Найди нам всё в большом прямоугольнике extendedBounds, исключая внутренний прямоугольник visibleBounds”. QuadTree отсекает ветки, целиком лежащие внутри экрана, избавляя нас от лишней пост-фильтрации элементов.

Все эти расчеты мы вынесли из главного потока в фоновую OperationQueue. Пользователь постоянно крутит карту, генерируя кучу жестов. Если повесить пространственные запросы на main thread, интерфейс гарантированно начнет собирать микрофризы.

Итоги

В итоге наша архитектура взаимодействия с картой выглядит так:

  • QuadTree структурирует точки в проекции Меркатора и отвечает за быстрые выборки по регионам.

  • Тап по экрану конвертируется в локальный поисковый запрос вокруг точки нажатия.

  • Сферическая геометрия связывает пиксели интерфейса с реальными метрами на поверхности Земли.

  • Тонкая фильтрация по правилу 20/60/20 спасает экран от визуальной перегрузки.

  • Буферизация зон убирает резкие скачки графики при перемещении по карте.

Замечание:

QuadTree – не единственный способ связывать пиксели с координатами. Есть ещё как минимум два метода:

  1. хэширование Идея: вместо дерева с переменным разрешением — фиксированная сетка ячеек одного размера, и каждая точка кладётся в ячейку по простому хэшу координат:

cellX = floor(x / cellSize)
cellY = floor(y / cellSize)
key   = hash(cellX, cellY)     // напр. cellX * 73856093 ^ cellY * 19349663
bucket[key].append(point)

Вставка/поиск ячейки за O(1), без спуска по дереву. Точность по построению: если cellSize ≥ радиуса коллизии r, то любой объект ближе r гарантировано лежит в текущей ячейке или одной из 8 смежных. Минус – на сильно неравномерной плотности (пустой океан + плотный город) тратит память на пустые ключи и теряет преимущество над деревом для range-запросов общего вида. Но для моей задачи в целом это то, что нужно: дёшево, точно, тривиально параллелится.

  1. k-d дерево Представьте, что у вас много точек, и вы постоянно делите пространство пополам: сначала по x, потом по y, потом снова по x, и так далее. В каждом узле дерева хранится одна точка и правило: всё, что левее/ниже неё, идёт в одну ветку, всё, что правее/выше — в другую. Поэтому, когда нужно найти ближайшую точку или все точки в области, можно не проверять все точки подряд, а быстро отбрасывать целые куски пространства. То есть k-d дерево — это как аккуратно разложенная карта точек, где поиск идёт не “по всем”, а “только туда, где может быть ответ”. Для поиска ближайших точек в абстрактном пространстве k-d дерево часто удобнее, чем QuadTree.