diff --git a/src/AppBundle/Command/LoadLDRawLibraryCommand.php b/src/AppBundle/Command/LoadLDRawLibraryCommand.php index 4a4fd93..cef5bd5 100644 --- a/src/AppBundle/Command/LoadLDRawLibraryCommand.php +++ b/src/AppBundle/Command/LoadLDRawLibraryCommand.php @@ -26,11 +26,12 @@ class LoadLDRawLibraryCommand extends ContainerAwareCommand //TODO log errors try { + // TODO handle relative path to dir if (($ldrawPath = $input->getArgument('ldraw_path')) == null) { $ldrawPath = $ldrawLoader->downloadLibrary(); } - $ldrawLoader->loadModels($ldrawPath); + $ldrawLoader->loadData($ldrawPath); } catch (\Exception $e) { printf($e->getMessage()); } diff --git a/src/AppBundle/Entity/LDraw/Part.php b/src/AppBundle/Entity/LDraw/Part.php index 7eeb1a7..a188389 100644 --- a/src/AppBundle/Entity/LDraw/Part.php +++ b/src/AppBundle/Entity/LDraw/Part.php @@ -4,12 +4,13 @@ namespace AppBundle\Entity\LDraw; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; /** * Part. * - * @ORM\Entity + * @ORM\Entity(repositoryClass="AppBundle\Repository\PartRepository") * @ORM\Table(name="ldraw_part") */ class Part @@ -43,18 +44,18 @@ class Part private $category; /** - * @var Part + * @var Part_Relation * - * @ORM\ManyToOne(targetEntity="AppBundle\Entity\LDraw\Part",cascade={"persist"}) + * @ORM\OneToMany(targetEntity="AppBundle\Entity\LDraw\Part_Relation", mappedBy="parent") */ - private $printOf; + private $relationsTo; /** - * @var Part + * @var Part_Relation * - * @ORM\ManyToOne(targetEntity="AppBundle\Entity\LDraw\Part",cascade={"persist"}) + * @ORM\OneToMany(targetEntity="AppBundle\Entity\LDraw\Part_Relation", mappedBy="child") */ - private $aliasOf; + private $relationsFrom; /** * @var Model @@ -73,6 +74,8 @@ class Part public function __construct() { $this->keywords = new ArrayCollection(); + $this->relationsTo = new ArrayCollection(); + $this->relationsFrom = new ArrayCollection(); } /** @@ -148,43 +151,35 @@ class Part } /** - * @return Part + * @return Part_Relation */ - public function getPrintOf() + public function getRelationsTo() { - return $this->printOf; + return $this->relationsTo; } /** - * @param Part $printOf - * - * @return Part + * @param Part_Relation $relationsTo */ - public function setPrintOf($printOf) + public function setRelationsTo($relationsTo) { - $this->printOf = $printOf; - - return $this; + $this->relationsTo = $relationsTo; } /** - * @return Part + * @return Part_Relation */ - public function getAliasOf() + public function getRelationsFrom() { - return $this->aliasOf; + return $this->relationsFrom; } /** - * @param Part $printOf - * - * @return Part + * @param Part_Relation $relationsFrom */ - public function setAliasOf($aliasOf) + public function setRelationsFrom($relationsFrom) { - $this->aliasOf = $aliasOf; - - return $this; + $this->relationsFrom = $relationsFrom; } /** @@ -193,11 +188,12 @@ class Part public function getModel() { if (!$this->model) { - if ($this->printOf) { - return $this->printOf->getModel(); - } elseif ($this->aliasOf) { - return $this->aliasOf->getModel(); + if ($this->getPrintOf()) { + return $this->getPrintOf()->getModel(); + } elseif ($this->getAliasOf()) { + return $this->getAliasOf()->getModel(); } + return null; } @@ -246,10 +242,80 @@ class Part * * @return Part */ - public function removePart(Keyword $keyword) + public function removeKeyword(Keyword $keyword) { $this->keywords->removeElement($keyword); return $this; } + + private function getRelationOf($type) + { + $criteria = new Criteria(); + $criteria->where(Criteria::expr()->eq('type', $type)); + + $relations = $this->relationsFrom->matching($criteria); + + $array = new ArrayCollection(); + foreach ($relations as $relation) { + $array->add($relation->getParent()); + } + + return $array; + } + + private function getRelations($type) + { + $criteria = new Criteria(); + $criteria->where(Criteria::expr()->eq('type', $type)); + + $relations = $this->relationsTo->matching($criteria); + + $array = new ArrayCollection(); + foreach ($relations as $relation) { + $array->add($relation->getChild()); + } + + return $array; + } + + public function getPrintOf() + { + $parents = $this->getRelationOf('Print'); + if (count($parents) > 0) { + return $parents->first(); + } + + return null; + } + + public function getPrints() + { + return $this->getRelations('Print'); + } + + public function getSubpartOf() + { + return $this->getRelationOf('Subpart'); + } + + public function getSubparts() + { + return $this->getRelations('Subpart'); + } + + public function getAliasOf() + { + $parents = $this->getRelationOf('Alias'); + if (count($parents) > 0) { + return $parents->first(); + } + + return null; + } + + public function getAliases() + { + return $this->getRelations('Alias'); + } } diff --git a/src/AppBundle/Entity/LDraw/Part_Relation.php b/src/AppBundle/Entity/LDraw/Part_Relation.php new file mode 100644 index 0000000..e0242e5 --- /dev/null +++ b/src/AppBundle/Entity/LDraw/Part_Relation.php @@ -0,0 +1,109 @@ +parent; + } + + /** + * @param Part $parent + */ + public function setParent($parent) + { + $this->parent = $parent; + } + + /** + * @return Part + */ + public function getChild() + { + return $this->child; + } + + /** + * @param Part $child + */ + public function setChild($child) + { + $this->child = $child; + } + + /** + * @return int + */ + public function getCount() + { + return $this->count; + } + + /** + * @param int $count + */ + public function setCount($count) + { + $this->count = $count; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $type + */ + public function setType($type) + { + $this->type = $type; + } +} diff --git a/src/AppBundle/Loader/LDrawLoader.php b/src/AppBundle/Loader/LDrawLoader.php index 7457699..16d9f64 100644 --- a/src/AppBundle/Loader/LDrawLoader.php +++ b/src/AppBundle/Loader/LDrawLoader.php @@ -6,14 +6,15 @@ use AppBundle\Entity\LDraw\Category; use AppBundle\Entity\LDraw\Keyword; use AppBundle\Entity\LDraw\Model; use AppBundle\Entity\LDraw\Part; +use AppBundle\Entity\LDraw\Part_Relation; use AppBundle\Entity\LDraw\Type; use AppBundle\Service\LDViewService; use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; use Symfony\Component\Asset\Exception\LogicException; use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Finder\Finder; +//TODO refactor class LDrawLoader extends Loader { /** @@ -21,6 +22,9 @@ class LDrawLoader extends Loader */ private $ldraw; + /** + * @var string download URL with current LDraw library + */ private $ldraw_url; /** @@ -37,6 +41,11 @@ class LDrawLoader extends Loader $this->ldraw_url = $ldraw_url; } + /** + * Download current LDraw library and extract it to system tmp directory. + * + * @return string Absolute path to temporary Ldraw library + */ public function downloadLibrary() { $this->output->writeln('Downloading LDraw library form ldraw.org'); @@ -57,19 +66,54 @@ class LDrawLoader extends Loader return $temp_dir; } - public function loadModels($LDrawLibrary) + /** + * @param $LDrawLibrary + */ + public function loadData($LDrawLibrary) { $adapter = new Local($LDrawLibrary); $this->ldraw = new Filesystem($adapter); -// $files = $this->ldraw->get('parts')->getContents(); + $this->loadParts(); + } + + /** + * Load stl model by calling LDViewSevice and create new Model. + * + * @param $file + * @param $header + * + * @throws \Exception + * + * @return Model|null + */ + private function loadModel($file, $header) + { + if (($model = $this->em->getRepository(Model::class)->find($header['id'])) == null) { + $model = new Model(); + $model->setId($header['id']); + } + + $model->setAuthor($header['author']); + $model->setModified($header['modified']); + + try { + $stlFile = $this->LDViewService->datToStl($file, $this->ldraw)->getPath(); + $model->setFile($stlFile); + } catch (\Exception $e) { + throw $e; //TODO + } + + return $model; + } + + // TODO refactor + public function loadParts() + { $files = $this->ldraw->get('parts')->getContents(); $this->em->getConnection()->getConfiguration()->setSQLLogger(null); -// $finder = new Finder(); -// $files = $finder->files()->name('*.dat')->depth('== 0')->in(getcwd().DIRECTORY_SEPARATOR.$LDrawLibrary.DIRECTORY_SEPARATOR.'parts'); - $progressBar = new ProgressBar($this->output, count($files)); $progressBar->setFormat('very_verbose'); $progressBar->setMessage('Loading LDraw library models'); @@ -110,40 +154,69 @@ class LDrawLoader extends Loader } } - if ($header['print_of_id']) { - if (($printParent = $this->em->getRepository(Part::class)->find($header['print_of_id'])) == null) { + if ($printParentId = $this->getPrinetedParentId($header['id'])) { + if (($printParent = $this->em->getRepository(Part::class)->find($printParentId)) == null) { $printParent = new Part(); - $printParent->setId($header['print_of_id']); + $printParent->setId($printParentId); + + if (!$this->ldraw->has('parts/'.$printParentId.'.dat')) { + $printParent->setModel($this->loadModel($file, $header)); + } } - $part->setPrintOf($printParent); + + if (($alias = $this->em->getRepository(Part_Relation::class)->find(['parent' => $printParent, 'child' => $part, 'type' => 'Print'])) == null) { + $alias = new Part_Relation(); + $alias->setParent($printParent); + $alias->setChild($part); + $alias->setCount(0); + $alias->setType('Print'); + } + $this->em->persist($alias); } - if ($header['alias_of_id']) { - if (($aliasParent = $this->em->getRepository(Part::class)->find($header['alias_of_id'])) == null) { - $aliasParent = new Part(); - $aliasParent->setId($header['alias_of_id']); + if (isset($header['subparts'])) { + $relationType = strpos($header['name'], '~Moved to ') === 0 ? 'Alias' : 'Subpart'; + + if ($header['type'] == 'Alias') { + if (count($header['subparts']) == 1) { + $relationType = 'Alias'; + } + } + + foreach ($header['subparts'] as $subId) { + if ($subId != $this->getPrinetedParentId($header['id'])) { + if ($this->ldraw->has('parts/'.$subId.'.dat') && $this->fileFilter($this->getPartHeader($this->ldraw->get('parts/'.$subId.'.dat')->getMetadata()))) { + if (($subPart = $this->em->getRepository(Part::class)->find($subId)) == null) { + $subPart = new Part(); + $subPart->setId($subId); + + $this->em->persist($subPart); + } + + if (($alias = $this->em->getRepository(Part_Relation::class)->find(['parent' => $part, 'child' => $subPart, 'type' => $relationType])) == null) { + $alias = new Part_Relation(); + $alias->setParent($part); + $alias->setChild($subPart); + $alias->setCount(0); + $alias->setType($relationType); + } + + $alias->setCount($alias->getCount() + 1); + + $this->em->persist($alias); + } + } } - $part->setAliasOf($aliasParent); } - if (!$header['print_of_id'] && !$header['alias_of_id']) { - if (($model = $this->em->getRepository(Model::class)->find($header['id'])) == null) { - $model = new Model(); - $model->setId($header['id']); - } + if (!in_array($header['type'], ['Print', 'Alias'])) { + $part->setModel($this->loadModel($file, $header)); + } - $model->setAuthor($header['author']); - $model->setModified($header['modified']); - - try { - $file = $this->LDViewService->datToStl($file, $this->ldraw)->getPath(); - } catch (\Exception $e) { - dump($e); - } - - $model->setFile($file); - - $part->setModel($model); + try { + $this->LDViewService->datToPng($file, $this->ldraw); + } catch (\Exception $e) { + dump($e->getMessage()); } $this->em->persist($part); @@ -156,15 +229,26 @@ class LDrawLoader extends Loader $progressBar->finish(); } + /** + * Determine if part file should be loaded into database. + * + * @param $header + * + * @return bool + */ private function fileFilter($header) { - if (strpos($header['name'], 'Sticker') !== 0 && - (strpos($header['name'], '~') !== 0 || strpos($header['name'], '~Moved to ') === 0) && - $header['type'] !== 'Subpart') { + // Do not include sticker parts and incomplete parts + if (strpos($header['name'], 'Sticker') !== 0 && strpos($header['id'], 's') !== 0 && $header['type'] != 'Subpart') { + // If file is alias of another part determine if referenced file should be included if (strpos($header['name'], '~Moved to ') === 0) { - $filepath = str_replace('\\', DIRECTORY_SEPARATOR, 'parts/'.$header['alias_of_id'].'.dat'); + // Get file path of referenced part file + $alias = 'parts/'.$this->getObsoleteParentId($header['name']).'.dat'; + if ($this->ldraw->has($alias)) { + return $this->fileFilter($this->getPartHeader($this->ldraw->get($alias)->getMetadata())); + } - return $this->fileFilter($this->getPartHeader($this->ldraw->get($filepath)->getMetadata())); + return false; } return true; @@ -173,11 +257,19 @@ class LDrawLoader extends Loader return false; } + /** + * Get printed part parent id. + * + * part name in format: + * nnnPxx, nnnnPxx, nnnnnPxx, nnnaPxx, nnnnaPxx (a = alpha, n= numeric, x = alphanumeric) + * + * http://www.ldraw.org/library/tracker/ref/numberfaq/ + * + * + * @param $filename + */ private function getPrinetedParentId($filename) { - // nnnPxx, nnnnPxx, nnnnnPxx, nnnaPxx, nnnnaPxx - // where (a = alpha, n= numeric, x = alphanumeric) - if (preg_match('/(^.*)(p[0-9a-z][0-9a-z][0-9a-z]{0,1})$/', $filename, $matches)) { return $matches[1]; } @@ -185,16 +277,19 @@ class LDrawLoader extends Loader return null; } - private function isShortcutPart($filename) - { - // nnnCnn, nnnnCnn, nnnnnCnn Shortcut assembly of part nnn, nnnn or nnnnn with other parts or formed version of flexible part nnn, nnnn or nnnnn. - // nnnCnn-Fn, nnnnCnn-Fn, nnnnnCnn-Fn Positional variant of shortcut assembly of movable parts, comprising part nnn, nnnn or nnnnn with other parts. - // where (a = alpha, n= numeric, x = alphanumeric) - - return preg_match('/(^.*)(c[0-9][0-9])(.*)/', $filename); - } - - private function getAliasParentId($name) + /** + * Get parent of obsolete part kept for reference. + * + * part description in format: + * ~Moved to {new_number} + * + * http://www.ldraw.org/article/398.html (Appendix II (02-Oct-06)) + * + * @param $name + * + * @return string|null Filename of referenced part + */ + private function getObsoleteParentId($name) { if (preg_match('/^(~Moved to )(.*)$/', $name, $matches)) { return $matches[2]; @@ -203,11 +298,21 @@ class LDrawLoader extends Loader return null; } + /** + * Get file reference from part line. + * + * Line type 1 is a sub-file reference. The generic format is: + * 1 x y z a b c d e f g h i + * + * LDraw.org Standards: File Format 1.0.2 (http://www.ldraw.org/article/218.html) + * + * @param $line + * + * @return string|null Filename of referenced part + */ private function getAlias($line) { - // 1 x y z a b c d e f g h i - - if (preg_match('/^1(.*) (.*)\.dat$/', $line, $matches)) { + if (preg_match('/^1(.*) (.*)\.(dat|DAT)$/', $line, $matches)) { return $matches[2]; } @@ -215,14 +320,27 @@ class LDrawLoader extends Loader } /** + * Parse LDraw .dat file header identifying model store data to array. + * + * [ + * 'id' => string + * 'name' => string + * 'category' => string + * 'keywords' => [] + * 'author' => string + * 'modified' => DateTime + * 'type' => string + * 'subparts' => [] + * ] + * + * LDraw.org Standards: Official Library Header Specification (http://www.ldraw.org/article/398.html) + * * @return array */ private function getPartHeader($file) { $header = []; -// $handle = fopen($file->getRealPath(), 'r'); - $handle = $this->ldraw->readStream($file['path']); if ($handle) { @@ -237,9 +355,9 @@ class LDrawLoader extends Loader // 0 if (!$firstLine) { - $array = explode(' ', trim($line), 2); - $header['category'] = isset($array[0]) ? ltrim($array[0], '=_~') : ''; - $header['name'] = $line; + $array = explode(' ', ltrim(trim($line, 2), '=_~')); + $header['category'] = isset($array[0]) ? $array[0] : ''; + $header['name'] = ltrim($line, '=_'); $firstLine = true; } @@ -262,7 +380,8 @@ class LDrawLoader extends Loader // 0 !LDRAW_ORG Part|Subpart|Primitive|48_Primitive|Shortcut (optional qualifier(s)) ORIGINAL|UPDATE YYYY-RR elseif (strpos($line, '!LDRAW_ORG ') === 0) { $type = preg_replace('/(^!LDRAW_ORG )(.*)( UPDATE| ORIGINAL)(.*)/', '$2', $line); - $header['type'] = $type; + + $header['type'] = in_array($type, ['Part Alias', 'Shortcut Physical_Colour', 'Shortcut Alias', 'Part Physical_Colour']) ? 'Alias' : $type; // Last modification date in format YYYY-RR $date = preg_replace('/(^!LDRAW_ORG )(.*)( UPDATE | ORIGINAL )(.*)/', '$4', $line); @@ -273,25 +392,17 @@ class LDrawLoader extends Loader } } } elseif (strpos($line, '1 ') === 0) { - if ($header['type'] == 'Part Alias' || $header['type'] == 'Shortcut Physical_Colour' || $header['type'] == 'Shortcut Alias') { - // "=" -> Alias name for other part kept for referece - do not include model -> LINK - // "_" -> Physical_color - do not include model -> LINK - $header['name'] = ltrim($header['name'], '=_'); - $header['alias_of_id'] = $this->getAlias($line); - } elseif ($header['type'] == 'Shortcut') { - $header['subparts'][] = $this->getAlias($line); - } + $header['subparts'][] = $this->getAlias($line); } elseif ($line != '') { break; } } - if (isset($header['id'])) { - $header['print_of_id'] = $this->getPrinetedParentId($header['id']); - } - - if (isset($header['name']) && !isset($header['alias_of_id'])) { - $header['alias_of_id'] = $this->getAliasParentId($header['name']); + if (strpos($header['name'], '~') === 0) { + $header['name'] = ltrim($header['name'], '~'); + $header['type'] = 'Obsolete/Subpart'; + } elseif ($printParentId = $this->getPrinetedParentId($header['id'])) { + $header['type'] = 'Print'; } fclose($handle);