Імпортуємо DXF креслення в програмі на Java, наступаючи на всі граблі цього «простого» формату

AutoCAD і подібні йому САПР давно вже стали стандартом в області проектування, і не дивно що таким же стандартом стали широко використовувані в них формати файлів DWG/DXF. Так що якщо ви розробляєте якесь рішення для архітекторів і проектувальників, то вміння працювати з цими форматами (ну або хоча б з одним з них) — must have фіча вашого продукту.



В рамках свого вебсервиса для симуляції руху пішоходів довелося і мені потурбуватися імпортом генпланів в цих форматах. Раніше з САПР я справи не мав, тому наївно думав «та що там, подумаєш — ще один формат креслень, лінії і багатокутники, що там може бути складного?». Але в процесі роботи з'ясувалося що складного там може бути достатньо, деякі нюанси цілком схожі на стародавні милиці, що тягнуться з глибин століть, при цьому багато речей толком не задокументовані в специфікаціях самого формату (наприклад робота з блоками або з кривими). Мабуть вони вважаються очевидними для будь-якого кресляра, але що робити якщо ви родом з іншої області, і таких знань не маєте?

Загалом під катом — перерахування граблів і рішень, які не вдалося нагуглити і довелося добувати полуночными пильнуваннями над кресленнями.


Розв'язувана задача

Мені для мого додатки на Java потрібно було налаштувати імпорт генпланів районів і конвертацію його у внутрішнє спрощене GeoJSON подання. При цьому мені не потрібна була повна інформація і всі види сутностей, лише деяка їх частина, яка б використовувалася в симуляції. Так що даний матеріал не охоплює всі можливості і Нетривіальні Технічні Рішення DXF. А чому саме DXF, а не DWG? А про це нижче.

формат

Отже, що в першу чергу асоціюється зі словами «автокад» і «формат файлу»? А це DWG. Бінарний закритий формат, який спочатку був створений AutoDesk'ом і його специфікації не розкривалися, однак в свій час він був вдало реверснут Open Design Alliance.
І ось тут слід розчарування №1: не існує актуальних безкоштовних реалізацій цього формату. Взагалі.
Є бібліотека по роботі з ним від AutoDesk. Є популярна бібліотека Teigha, створена ODA. І… все. Обидві вони платні, причому добре платні (мова про сотнях і тисячах доларів). Не підходить.
Є деяка кількість спроб реалізувати стандарт у вигляді безкоштовного Open-source рішення. Наприклад, jdwglib. Але всі вони давно мертві, оновлювалися останній раз 5-10 років тому. А прогрес не стоїть на місці, нові версії автокад додають нові фічі і в DWG, в підсумку з мрією читати файли сучасних версій можете попрощатися, як і з підтримкою і надією на фікс багів.

Альтернативою є DXF. Трохи менш популярний, але в той же час підтримуваний всіма САПР, спочатку відкритий і тому, по ідеї, більш поширений.
Пошук бібліотек спочатку теж бентежить — конкретно для Java немає жодного живого проекту, всюди та ж картина: останні релізи 5-річної давності, занедбані репозиторії, сумно дивляться у вічність останні новини, повні неоправдавшегося оптимима і обіцянок. Але сам по собі формат на відміну від DWG не так активно розвивається, тому навіть досить старої бібліотекою цілком можна відкрити актуальні креслення.

У підсумку була обрана бібліотека Kabeja, останній реліз якої був в 2011 році. За допомогою наявного в комплекті семпла (конвертація DXF в SVG) було перевірено всі актуальні файли креслень коректно відкриваються, після чого я приступив до імпорту. Злегка правда насторожив мене один коментар до питання про парсинг DXF від якогось CAD-гуру на Stackoverflow що, мовляв, «DXF виглядає простим але насправді ти запаришься з ним працювати».

Шари

DXF креслення містить у собі набір шарів (layer) і блоків (block). Там є і інші сутності, але для того щоб видерти координати геометрії в найпростішому випадку вони не потрібні.



З шарами все очевидно, працюють вони так само як і в якому-небудь Photoshop. Шари можна вмикати-вимикати і можна ставити дефолтні налаштування графіки для шару (тобто наприклад всі лінії за замовчуванням на даному шарі будуть мати таку товщину). Оскільки моїм завданням була тільки вичавка координат, питаннями відображення я не займався.

Окей, все здається просто: біжимо за списком шарів, для кожного шару — за списком об'єктів, перетворимо координати. Втім вже тут я наступив на перші граблі: набір шарів який ви бачите в CAD і який є у файлі — це не одне і те ж. Я собі голову зламав, чому у мене раптом зникали шматки доріг. В NanoCAD вони є, в моєму експорті — ні. Поліз в відладчик — їх і повертаються Kabeja структурах немає. Зате якщо експортувати файл цілком їх семплом вони є. Загалом з'ясувалося, що один шар з редактора у файлі може представлятися декількома шарами, з іменами виду «layerName», «layerName @ 1». Навіщо це зроблено, і звідки воно береться — чорт його знає, але факт — пошук на точний збіг імені шару (на який натякає навіть структура коду бібліотеки, зберігає шари Map з ключем-ім'ям) не працює.

Блоки

Блоки являють собою шаблони, які неодноразово намалювавши можна багаторазово вставляти. При цьому зміна базового блоку змінить і всі його вставки. Зручно. Ще крутіше те, що блок може містити в собі об'єкти з декількох шарів. При цьому вставка теж належить якомусь шару. При цьому блок може містити в собі вставки інших блоків. Тобто можна зробити блоки «секція будинку», потім скласти з них блок «дім», який потім кілька разів вставити карту. При цьому підсумковий об'єкт буде мати кілька шарів (окремо заливка, окремо контури, окремо спеціальні позначки), так само як вихідні блоки. Все це дуже круто з точки зору користувача, але додає роботи програміста.
Більш того, блок може вставлятися не просто разово, а багаторазово у вигляді прямокутної матриці. Для цього у вставки об'єкта є параметри з кількістю рядків, стовпців і з відстанню між ними.

У результаті поки що код обробки вставки виглядає якось так:

for (int row = 0; row < insert.getRows(); ++row) {
for (int col = 0; col < insert.getColumns(); ++col) {
// Нам треба перетворити координати точок локальних координат блоку в глобальні координати креслення з урахуванням місця вставки, кута повороту і т. п
AffineTransform transform = new AffineTransform();
transform.translate(
insert.getPoint().getX() - (insert.getColumns() - col) * insert.getColumnSpacing()
, insert.getPoint().getY() - (insert.getRows() - row) * insert.getRowSpacing());
transform.rotate(Math.toRadians(insert.getRotate()));
transform.scale(insert.getScaleX(), insert.getScaleY());
// Feature це вже наш об'єкт, що містить перетворені в GeoJSON координати
for (Feature f : block.features) {
// Копіюємо вміст блоку, перетворимо координати. У підсумку отримуємо реальний стан будинку на плані 
Feature inserted = cloneFeatureWithTransform(f, transform);
features.add(inserted);
}
}
}
return block.features.size();


При цьому важливо знати, що блоки і верстви в файлі не впорядковані. Тобто по черзі обробляючи блоки можна натрапити в ньому на вставку з іншого блоку, який ще не був оброблений. Чесно кажучи не знаю, чи можна примудритися зробити цикл і що в такому разі буде.

Лінії

Нагадаю, що моє завдання — перетворити DXF в GeoJSON, який з усіх видів геометрії визнає лише ламану і багатокутник, ніяких дуг кривих.

DXF підтримує купу різних варіантів ліній:

  1. Аж 2 типу ламаних — Polyline і LWPolyline. В моєму випадку простих 2д креслень різниці між ними ніякої
  2. Дуги, причому аж двох видів — еліптичні і кругові. На щастя в класах Kabeja вже є готові методи для отримання координат точок на них, так що перетворити дугу ламану з потрібною точністю нескладно
  3. Сплайни — знову ж Kabeja сама вміє перетворювати їх в Polyline
  4. Просто лінійні відрізки


Здавалося б все просто, але немає. Навіть простий на перший погляд тип Polyline може використовуватися для відображення кривих другого порядку (а не просто ламаних). Для цього у вершини може бути заданий параметр bulge. Якщо він вказаний дві вершини з'єднуються прямою лінією, а дугою кола, що проходить через ці вершини і центр якої можна виразити через них і цей параметр.



Ось такий код дозволяє визначити центр кола:
private Point getCenterByVerticesAndBulge(DXFVertex a, DXFVertex b, double bulge) {

double norm = Math.sqrt(Math.pow(b.getX() - a.getX(), 2) + Math.pow(b.getY() - a.getY(), 2));
double s = norm / 2;
double d = s * (1 - bulge * bulge) / (bulge * bulge);

// "direction"
double u = (b.getX() - a.getX()) / norm;
double v = (b.getY() - a.getY()) / norm;

//"center"
double c1 = Math.signum(bulge) * -v * d + (a.getX() + b.getX()) / 2;
double c2 = Math.signum(bulge) * u * d + (a.getY() + b.getY()) / 2;
return new Point(c1, c2, 0);
}


Над цими дугами я бився довго, та в результаті плюнув і відімкнув їх, поєднуючи вершини безпосередньо. Так як в генпланах такі дуги зазвичай використовують для заокруглень на кутах перехресть, я міг на них цілком забити — різниця з точки зору симуляції невелика.

Заливки/штрихування (hatch)

Це те з чим безпосередньо працює мій симулятор. Будівлі, дороги і генератори я поки вимагаю задавати саме заливками (інакше незрозуміло де в мішанині окремих ліній внутрішня частина, а де — зовнішня).

І тут теж є нюанси:
  1. Кордоном заливки може бути будь-яка комбінація об'єктів-ліній. Частина кордону може бути ламаної, потім кілька дуг, потім просто купа різних відрізків ліній
  2. Один об'єкт заливки може мати довільне число неперетинних областей (що нехарактерно для багатьох інших форматів де у багатокутника є лише одна зовнішня межа), кожна з яких може мати довільне число дірок
  3. Заливки можуть бути багаторазово вкладеними: тобто всередині заливки дірка, в якій ще одна заливка, в якій знову дірка, причому все це в DXF задається одним об'єктом HATCH з кількома кордонами.
  4. Є і ще більш екзотичні варіанти відносин заливки та її меж (див. картинку) але мені вони поки що слава богу не траплялися




Загалом з DXF для заливки ми отримуємо купу кордонів, мають прапор «зовнішня чи внутрішня», а далі самі повинні якось розбиратися як вони взагалі зроблені і як їх розкидати по GeoJSON-івським полігонів, які можуть мати тільки одну зовнішню кордон та не мають вкладеності.

Я сходив по декількох шляхах, але на кожен алгоритм я досить швидко отримував креслення, на якому цей алгоритм не працював. Наприклад ось цей ось: схема вулиць і проїздів для житлового району Чити, в якому вони всі задані буквально парою об'єктів HATCH з дуже складною структурою, в якій чомусь усі кордони були помічені як зовнішні (чую тут якийсь баг в Kabeja, так як DXF визначає відразу два схожих прапора External і Outer, але в самій бібліотеці є тільки один):



У результаті єдиний робочий алгоритм виглядає так:
  1. Створити полігон для кожної зовнішньої межі
  2. Відняти з нього всі інші кордони, неважливо відзначені вони зовнішніми чи внутрішніми
  3. Виправити можливі проблеми (полігон вийшов порожній, дірки виходять за межі зовнішнього контуру, полігон розбився на незв'язані області тощо)


Для третього пункту (та й взагалі для роботи з геометрією вже всередині самого алгоритму симуляції) я використовував бібліотеку JTS — Java Topology Suite. Вона містить досить багато всяких потрібних примітивів і операцій по роботі з геометрією, починаючи від операцій типу побудови буфера і закінчуючи структурами даних типу квадродерева.

Перемога?

Після багатьох мук і підперши все що можна милицями, я все-таки зміг створити підтримку потрібного мені підмножини DXF і завантажувати креслення безпосередньо у свій симулятор, щоб використовувати їх для визначення косяків проектувальників. Оскільки більшу частину вказаної вище інформацією мені довелося добувати з боєм і сидінням до двох ночі над NanoCAD-ом (не реклама, але це єдиний доступний безкоштовний і якісний редактор DXF який я знайшов, той же LibreCAD не подужав відкрити правильно перший же креслення що я йому передав), я вирішив поділитися нею і з хабрасообществом — раптом мій досвід заощадить кому-то час.

Ну і так, ось як-то так виглядає прогноз мого алгоритму для району зі скріншоту вище:



Очевидні висновки — не треба робити доріжки під прямим кутом, але не треба намагатися робити якісь незрозумілі заокруглення там де не треба. Подивимося через кілька років, справдиться мій прогноз: коли цей район побудують.

За надані плани районів спасибі Майстерні комплексного архітектурно-будівельного проектування
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.