понедельник, 12 октября 2009 г.

Интеграция TDMS и AutoCAD 2

На новом месте работы, где я занят внедрением системы электронного архива и документооборота TDMS в основном производстве, как и на прежнем месте, столкнулся с той же задачей, что решал раньше, а именно, интеграцией Autodesk AutoCAD и TDMS. Разумеется, имея ранее полученный опыт, решить аналогичную задачу оказалось проще. Но, как всегда бывает, в каждой ситуации возникают свои особенности…
Во-первых, в прежнем решении я отталкивался от того, что вся графическая документация оформляется по принципу "один файл - один лист". Теперь я столкнулся с тем фактом, что на предприятии активно используется оформление чертежей с использованием листов (Layout'ов), т.е. оформляется по принципу "один файл – несколько листов".
Во-вторых, как выяснилось позже, мной не был учтен тот факт, что если имена файлов объекта зависят от значения атрибута, который может изменится, то связи внутри компонентного документа нарушаются. Такое изменение необходимо отслеживать.
В-третьих, использование последовательной нумерации файлов внутри объекта имеет отрицательную сторону в том случае, если изменяется порядок файлов являющихся компонентами главного файла.
Преодоление первого препятствия.
Обдумывание решения о хранении многолистового документа привело к решению, простому как "три копейки". Если один файл хранит несколько листов, значит в TDMS должен быть объект-контейнер, который также может хранить несколько объектов. Таким образом совместно с коллегами было решено создать два типа объектов для хранения информации о чертежах: Чертеж и Лист.
На объект Чертеж возложена почетная обязанность хранить главный файл dwg и другие сопутствующие ему файлы (контекстный, растровые и компоненты, если надо). Набор атрибутов весьма скромен, в основном обозначение и количество листов, причем оба атрибута заполняются автоматически.
На объект Лист возложена ответственная обязанность хранить всю информацию о листе, который он олицетворяет. В частности, обозначение, наименование, перечень ответственных лиц, формат листа и т.п.
Таким образом, в TDMS хранится информация, как о файле, так и обо всех листах в нем размещенных, правда при работе с файлом пришлось ввести некоторые правила.
Правило 1.
Имя листа (layout) должно соответствовать номеру листа в TDMS.
Правило 2.
Количество листов в файле неограниченно и не контролируется, т.к. отследить все мыслимые манипуляции пользователя с листам практически невозможно. Однако, при переключении листов происходит проверка имени листа и, если в TDMS о нем нет информации, то AutoCAD выдает соответствующее предупреждение, но не запрещает пользователю с ним работать в дальнейшем.
Технически, реализация отслеживания переключения листов довольно проста: AlxdLSwitcher.rar
Обоим типам объектов соответствует команда Открыть, которая открывает документ в приложении. В случае, если команда вызвана объектом Лист то, кроме открытия файла, происходит переключение на соответствующий лист в нем. Таким образом, пользователям обеспечена предельная прозрачность работы с файлами через объекты TDMS. Напротив, если файл уже открыт в приложении, то TDMS просто активизирует его и переключит на нужный лист при необходимости.
Реализация кода на VBscript описанной функциональности не представляет труда: cmd_drawing_open.rar
Преодоление второго и третьего препятствий
Исключить проблему с именованием файлов объекта, было решено изменением принципа именования. Прежде всего, отказался от последовательной нумерации файлов в объекте. Однако, оставить имя файла самим по себе нельзя, т.к. возможны ситуации, когда появится два файла с одинаковым именем в одном объекте. Вместо номера файла появился четырехзначный уникальный идентификатор.
Отслеживание копирования или изменения обозначения объекта, которое влечет за собой изменение имен файлов реализовано непростым механизмом. В объекте TDMS присутствует служебный табличный атрибут, который хранит историю изменения имен файлов объекта. Анализируя его содержимое можно сделать вывод о том, какое имя файла должно быть у соответствующего файла в настоящий момент. Дополнительный модуль написанный на Lisp берет на себя такую ответственность. Функция (copy2tdms) вызывается при каждом открытии файла и проверяет необходимость изменения имен файлов.
В остальном, никаких особых препятствий преодолевать не пришлось, реализация интеграции AutoCAD и TDMS осталась очень похожа на описанную в предыдущей записи блога. Исходный код основных модулей:
Преимущества решения для инженера:
  • прозрачность работы с файлами и листами из TDMS
  • исключение операций с файлами при их переименовании
  • возможность использовать для оформления листы в чертеже AutoCAD
Недостатки:
  • если растровое изображение уже размещено на T:, но удалено из объекта, то код не отслеживает этого факта и файл может быть утерян.
Применение:

воскресенье, 11 октября 2009 г.

Интеграция TDMS и AutoCAD

Известно, что TDMS использует и ориентируется на механизм захвата и возврата (CheckOut/CheckIn в терминах программиста и команды Сохранить, Сохранить и закрыть, Отменить редактирование в терминах пользователя в меню Редактировать->Редактируемые объекты) файлов, хранящихся в объектах, который универсален, но применим больше для распределенной работы. Однако такой механизм при работе в рамках единого информационного пространства одного офиса не устраивает. Было принято решение реализовать работу с файлами по-своему.

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

Во-вторых, захваченные файлы, следуя идеологии TDMS, предполагается размещать на локальном диске рабочей станции пользователя. Следовательно, ни о какой интерактивности в работе и речи быть не может, т.е. исключается возможность online режима работы с внешними ссылками в AutoCAD'е. Уведомления в AutoCAD'е об изменении файла внешней ссылки не появятся никогда. Обидно терять такой функционал.

Как сделать так, чтобы файлы хранились в TDMS, но работа с ними была прозрачной? Чтобы решить задачу были выработаны следующие критерии:

  1. Файлы должны именоваться предсказуемо, т.е. имя файла должен давать не пользователь, а TDMS и не позволять именовать иначе.
  2. Файл должен захватываться на сетевой ресурс (файловый сервер), а не на локальный диск рабочей станции.
  3. Пути к файлам и компонентам не должны меняться без серьезных оснований.
  4. Файл должен захватывать за собой все компоненты (внешние ссылки и растры).
  5. Файл после сохранения или закрытия сразу должен попадать в TDMS.
  6. Файл после сохранения или закрытия должен сохранять вместе с собой растровые изображения, если они были изменены.

Этап 1 – подготовка к захвату.

Практически любая операция, изменяющая состав файлов в объекте TDMS влечет за собой вызов кода отвечающего за именование файлов в объекте. Чтобы имена файлов не совпадали, был досконально изучен документ, определяющий обозначение проектной документации и алгоритм размещения файлов в TDMS. В итоге, был принят следующий принцип:

Nindex description.ext, где

index – порядковый номер файла в коллекции;

description – наименование объекта, которое опирается на обозначение документа и стремится к уникальности;

ext – расширение файла

Причем из имени файла сразу исключаются недопустимые символы: косая черта, двоеточие, кавычки.

Выглядит это примерно так:

Таким образом, выполнились требования пункта 1.

Этап 2 – место для захвата.

Сетевой ресурс для размещения файлов создать было несложно. Файловый сервер уже есть, выделили каталог, расшарили, дали полные права пользователям и подключили ко всем рабочим станциям как диск P.

Получили \\server\tdms подключенный как диск P:.

Кроме этого, сразу пришлось отказаться от того, как TDMS формирует путь к захваченным файлам. Пришлось написать функции возвращающую новый путь захвата файла.

Было: Vol:\Catalog\UserName – GUID\filename.ext.

Стало: Vol:\GUID\filename.ext

Отказать от GUID нельзя, т.к. только по нему можно определить, к какому объекту относится файл.

Таким образом, выполнились требования пунктов 2 и 3.

Этап 3 – собственно сам захват.

Открытие файла из TDMS для пользователя должно происходит предельно прозрачно, он не должен задумываться, что происходит в системе, но должен в итоге увидеть открытый файл в соответствующем приложении.

В TDMS с привязкой к типу файла были написаны две команды: Открыть и Просмотр. Открытие файла в AutoCAD предполагает:

  • открытие AutoCAD'а, если он закрыт;
  • поиск открываемого файла в открытом AutoCAD'е, вдруг он уже открыт;
  • проверка прав пользователя на открытие для внесения изменений;
  • проверка возможности открытия файла, вдруг он открыт другим пользователем;
  • открытие файла.

Открыть AutoCAD на VBScript, тривиальная задача. Была написана следующая функция:

Function InitApp
Set InitApp = Nothing
dim acadApp
On Error Resume Next
Set acadApp = GetObject(,"AutoCAD.Application")
if Err then
Err.Clear
Set acadApp = CreateObject("AutoCAD.Application")
end if

if Err then
Err.Clear
MsgBox "Невозможно открыть Autodesk AutoCAD! Попробуйте открыть его самостоятельно и повторите команду.", 64, "Ошибка"
end if

acadApp.Visible = true

if acadApp.Documents.Count > 0 then
dim i
for i = 0 to 5000
if acadApp.GetAcadState.IsQuiescent = true then exit for
next
if acadApp.GetAcadState.IsQuiescent = false then
MsgBox "Autodesk AutoCAD занят выполнением команды. Дождитесь завершения или отмените ее!", 64, "Ошибка"
exit function
end if
end if

Set InitApp = acadApp
end Function

Хоть функция и ожидает открытия AutoCAD'а, все-таки пользователям рекомендовано самостоятельно открывать AutoCAD, чтобы не создавать недоразумений. Некоторые категории пользователей не подождав и секунды начинают судорожно повторять вызов команды Открыть. Человеческий фактор.

Поиск открываемого файла в сеансе AutoCAD тоже несложен

dim found
dim doc
found = false
For Each doc In app.Documents
If doc.FullName = ThisApplication.ExecuteScript("MAIN", "WorkFileName_TechDoc", main_file) Then
Set OpenFile = doc
doc.Activate
found = True
exit for
end if
Next

Функция WorkFileName_TechDoc из модуля MAIN как раз возвращает полный путь к захватываемому файлу.

Проверку прав пользователя на открытие файла написал мой коллега, кроме этого, код зависит от структуры прав и т.д., поэтому приводить ее код не буду. Функция просто возвращает readonly=true/false в зависимости от прав.

Проверка возможности открытия файла, если он вдруг открыт другим пользователем с сетевого ресурса, наиболее интересная задачка. Коллегой был написан скрипт, выполняемый на стороне сервера, который заполняет на шаре \\server\tdms файл access.log каждые 10 секунд. В файле формируется список вида:

С помощью регулярных выражений уже на стороне клиента в файле access.log ведется поиск записи о недоступности файла и если он действительно открыт другим пользователем, то пользователь получает соответствующее уведомление, а также предложение открыть его в режиме только для чтения.

Открытие файла и его компонент, простой код: Отдельно можно остановиться на том, как хранится информация о компонентах. В объекте существует специальный табличный атрибут, который содержит ссылки на все объекты, в которых хранятся компоненты настоящего документа, если таковые имеются. Соответственно, при попытке открыть документ, сначала открываются (читай, захватываются) все компоненты и только потом сам документ.

Функция захвата компонент:

Function CheckOutComponents(Obj)
if Obj.Attributes.Has("TECHDOC_COMPONENTS") then
dim attr
Set attr = Obj.Attributes("TECHDOC_COMPONENTS")
dim row
on error resume next
For Each row in attr.Rows
dim file
if not row.Attributes("TECHDOC_COMPONENTS_LINK").Object is nothing then
For Each file In row.Attributes("TECHDOC_COMPONENTS_LINK").Object.Files
file.CheckOut WorkFileName_TechDoc(File)
if err then
err.clear
end if
Next
end if
Next
end if
End Function

Фрагмент кода открытия файла:

dim file
For Each file In real_obj.Files
file.CheckOut ThisApplication.ExecuteScript("MAIN", "WorkFileName_TechDoc", File)
Next
Set doc = app.Documents.Open(ThisApplication.ExecuteScript("MAIN", "WorkFileName_TechDoc", main_file), mode)

Из кода видно, что при захвате файла вместе с ним захватываются и все файлы объекта, среди которых могут также оказаться компоненты (в основном растровые файлы).

Таким образом, выполнились требования пункта 4.

Этап 4 – сохранить и закрыть.

Наиболее сложным и интересным оказалось создание кода, обрабатывающего сохранение и закрытие файла в AutoCAD'е. Ответственность кода и необходимость его оперативного изменения в случае обнаружения ошибок, а также возможность корректировки другими сотрудниками во время моего отсутствия или занятости привело к выбору Lisp'а в роли платформы для реализации кода.

Код условно разделен на три составляющие:

  • сохранение растровых компонент и информации о них;
  • сохранение информации о внешних ссылках;
  • сохранение самого файла.

Объектная модель AutoCAD и информация о примитивах, предоставляемая в Lisp, очень скупы при работе с растровыми изображениями, поэтому написание соответствующего кода отняло некоторое время.

Фрагмент кода работы с растровыми изображениями:

(defun hasImage( / dict ret)
(setq
dict (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Dictionaries)
dict (vl-catch-all-apply 'vlax-invoke-method (list dict 'Item "ACAD_IMAGE_DICT"))
ret nil
)

(if (not (vl-catch-all-error-p dict))
(setq ret (> (vlax-get-property dict 'Count) 0))
)

ret
)

(defun listImage( / dict objimage ret)
(setq
dict (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Dictionaries)
dict (vl-catch-all-apply 'vlax-invoke-method (list dict 'Item "ACAD_IMAGE_DICT"))
ret nil
)

(if (not (vl-catch-all-error-p dict))
(vlax-for objimage dict
(setq ret (append ret (list (cdr (assoc 1 (entget (handent (vlax-get-property objimage 'Handle))))))))
)
)
ret
)

Фрагмент кода отвечающего за сохранение растрового изображения средствами Raster Design:

(defun try_save_image_by_co ( fullname pathlist / coList coFileName coWrite ic cc imID )
;fullname must be is raster image file name
(if (/= (member "aeciui42.arx" (arx)) nil)
(progn
(princ "\nRaster Design загружен.")
(setq coList (vlax-invoke-method (vlax-get-acad-object) 'GetInterfaceObject "CADOverlay.AecImageObjectList"))
(setq ic 0 cc (vlax-get-property coList 'CurrentSpaceCount))
(if (> cc 0)
(progn
(setq coFileName (vlax-invoke-method (vlax-get-acad-object) 'GetInterfaceObject "CADOverlay.AecCoImageInfo"))
(while (<>
(setq imID (vlax-invoke-method coList 'CurrentSpaceObjectID ic))
(vlax-put-property coFileName 'ImageObjectID imID)
(if (= (vlax-get-property coFileName 'SavedImageFilePath) fullname)
(progn
(setq coWrite (vlax-invoke-method (vlax-get-acad-object) 'GetInterfaceObject "CADOverlay.AecImageWrite"))
(vlax-put-property coWrite 'ImageObjectID imID)
(vlax-invoke-method coWrite 'SaveAs fullname)
(vlax-release-object coWrite)
(princ (strcat "\n" (nth 2 pathlist) "\tсохранен."))
)
)

(setq ic (1+ ic))
)))

(vlax-release-object coFileName)
(vlax-release-object coList)
)

(princ "\nRaster Design не загружен.")
);if member
T
)

Помимо простого сохранения и сдачи растровых изображений хранимых в чертеже в TDMS, код распознает изображения подключенные не с диска P: и автоматически помещает их в объект, в котором хранится чертеж. Таким образом, пользователь избавлен от необходимости импортировать растр в TDMS, а потом организовывать на него ссылку.

Фрагмент кода отвечающего за изменение имени файла растрового изображения:

(defun changeImageFileName ( oldfileName newFileName / dict objimage obj)
(setq
dict (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Dictionaries)
dict (vl-catch-all-apply 'vlax-invoke-method (list dict 'Item "ACAD_IMAGE_DICT"))
)

(if (not (vl-catch-all-error-p dict))
(vlax-for objimage dict
(setq obj (entget (handent (vlax-get-property objimage 'Handle))))
(if (= (cdr (assoc 1 obj)) oldFileName)
(progn
(setq obj (subst (cons 1 newFileName) (assoc 1 obj) obj))
(entmod obj)
)))))

Управление внешними ссылками намного проще, да и сохранять или сдавать их не надо. Достаточно только обновить информацию в соответствующем атрибуте. Причем список ссылок в табличном атрибуте просто полностью очищается и заполняется вновь, чтобы не усложнять код.

Фрагмент кода работы с внешними ссылками:

(defun hasXref( / ablocks i cnt ret)
(setq
ablocks (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Blocks)
cnt (vlax-get-property ablocks 'Count)
i 0
ret nil
)

(while (and (<>
(setq
ret (if (= (vlax-get-property (vlax-invoke-method ablocks 'Item i) 'IsXRef) :vlax-true) T)
i (1+ i)
))

(vlax-release-object ablocks)
ret
)

(defun listXref( / ablocks ablock ret)
(setq
ablocks (vlax-get-property (vlax-get-property (vlax-get-acad-object) 'ActiveDocument) 'Blocks)
ret nil
)

(vlax-for ablock ablocks
(if (= (vlax-get-property ablock 'IsXRef) :vlax-true)
(setq ret (append ret (list (vlax-get-property ablock 'Path))))
))

(vlax-release-object ablocks)
ret
)

Фрагмент кода очистки табличного атрибута:

(setq
attrRows (vlax-get-property TDMSAttribute 'Rows)
)

(while (> (vlax-get-property attrRows 'Count) 0)
(vlax-invoke-method attrRows 'Remove 0)
)

Фрагмент кода заполнения табличного атрибута:

(setq
attrRows (vlax-get-property TDMSAttribute 'Rows)
)

(setq
attrRow (vlax-invoke-method attrRows 'Create)
attrRow (vlax-get-property (vlax-get-property attrRow 'Attributes) 'Item 0)
)

(vlax-put-property attrRow 'Object (car component))

Перехват событий при сохранении и закрытии осуществлен с помощью реакторов: :vlr-saveComplete и :vlr-beginClose. Закрытие перехватывается еще и для создания файла индексов, о котором было сказано в блоге TDMS и NormaCS.

Фрагмент кода отвечающего за сдачу файла:

;pathlist – список с разбитым на части полным именем файла vol, guid, filename
(defun file_checkout ( fullname pathlist / TDMSObj TDMSFiles TDMSFile )
(setq TDMSObj (vlax-invoke-method TDMS 'GetObjectByGUID (nth 1 pathlist)))
(if (not TDMSObj)
(progn
(princ (strcat "\n" (nth 2 pathlist) "\tне сдан. Объект для файла не найден."))
(vl-exit-with-value 3)
))

(setq TDMSFiles (vlax-get-property TDMSObj 'Files))
(vlax-for TDMSFile TDMSFiles
(if (= (nth 2 pathlist) (vlax-get-property TDMSFile 'FileName))
(progn
(vlax-invoke-method TDMSFile 'CheckIn fullname)
(princ (strcat "\n" (nth 2 pathlist) "\tсдан."))
)))

(vlax-release-object TDMSFiles)
(vlax-release-object TDMSObj)
T
)

В результате, выполняя сохранение файла, пользователь абсолютно прозрачно и незаметно для себя помещает всю необходимую информацию в TDMS, включая самую актуальную версию файла.

Таким образом, выполнились требования пунктов 5 и 6.

Кроме этого, попутно было написано небольшое приложение на ObjectARX под названием AlxdIntercept.arx, которое отслеживает попытку подключить внешнюю ссылку в чертеж из стандартного менеджера внешних ссылок и, в случае, если открытый файл размещен в TDMS, подменяет стандартное окно выбора файла на окно выбора объекта из TDMS.

Преимущества решения для инженера:

  • исключение встроенного механизма захвата/сдачи из работы;
  • практически полное исключение операций помещения растровых изображений в TDMS вручную;
  • прозрачная работа с файлами, т.е. пользователь, как будто работает с файловой системой;
  • режим online для внешних ссылок, т.к. они все ссылаются на общий сетевой ресурс, и каждый пользователь мгновенно получает сообщение об изменении внешней ссылки средствами AutoCAD'а;
  • вставка внешних ссылок не требует дополнительных команд и выполняется стандартными средствами AutoCAD'а с минимальным вмешательством.

Недостатки:

  • если растровое изображение уже размещено на P:, но удалено из объекта, то код не отслеживает этого факта и файл может быть утерян;
  • изменение путей к растрам при сохранении срабатывает только после "двойного" сохранения, что при сохранении и так выполняется, а при закрытии не срабатывает, поэтому путь к растру при первом закрытии с сохранением не изменится;
  • код опирается на именование файлов.

Нехватка:

  • отсутствие методов или свойств управления порядком файлов в коллекции файлов (например, методов Up и Down или Swap) объекта TDMS не позволяет написать код, отслеживающий вредительство пользователей, например, удаление востребованного растра из объекта, изменение индекса файла в коллекции файлов являющегося компонентом документа и т.п.
Применение: