diff --git a/README.md b/README.md index ee13d9a..fb24ea2 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,5 @@ For full requirements see Symfony 3.2 [docs](http://symfony.com/doc/3.2/referenc 1. Set application parameters in *app/config/parameters.yml* 2. Generate empty database by running command `$ php bin/console doctrine:database:create` 3. Load LDraw models into database by running commad `$ php bin/console app:load:ldraw [ldraw_dir_path]` -4. Load Rebrickable data into database by running command `$ php bin/console app:load:rebrickable` \ No newline at end of file +4. Load Rebrickable data into database by running command `$ php bin/console app:load:rebrickable` +5. Load relations between LDraw models and Rebrickable parts by running command `$ php bin/console app:load:relation` \ No newline at end of file diff --git a/app/Resources/relations/alias_model.yml b/app/Resources/relations/alias_model.yml new file mode 100644 index 0000000..c602122 --- /dev/null +++ b/app/Resources/relations/alias_model.yml @@ -0,0 +1,167 @@ +# Define the relationship between LDraw models that are not automatically recognizabe to eliminate duplication of models +# +# format: +# alias_ID: model_ID + +# Minifig Hips and Legs +3815c3j: 3815c01 +3815c4f: 3815c01 +3815c63: 3815c01 +3815c6g: 3815c01 +3815c6u: 3815c01 +3815c6v: 3815c01 +3815c6w: 3815c01 +3815c89: 3815c01 +3815c8h: 3815c01 +3815c8i: 3815c01 +3815ca2: 3815c01 +3815ca3: 3815c01 +3815ca9: 3815c01 +3815caw: 3815c01 +3815cba: 3815c01 +3815cbb: 3815c01 +3815cbc: 3815c01 +3815cbd: 3815c01 +3815cbe: 3815c01 +3815cbf: 3815c01 +3815cbg: 3815c01 +3815cc44: 3815c01 +3815cc67: 3815c01 +3815cde: 3815c01 +3815chb: 3815c01 +3815cm0: 3815c01 +3815cm1: 3815c01 +3815cm2: 3815c01 +3815cq0: 3815c01 +3815cq1: 3815c01 +3815cq2: 3815c01 +3815cs5: 3815c01 +3815csk: 3815c01 +3815cw1: 3815c01 +3815cw2: 3815c01 +3815cw3: 3815c01 +4214921: 3815c01 +4221858: 3815c01 +4225787: 3815c01 +4168950: 3815c01 +73418: 3815c01 + +122c02: 122c01 + +# Bucket 1 x 1 x 1 Cylindrical +12884c02: 12884c01 + +11244p02c01: 11244p01c01 + +16529p02c01: 16529p01c01 + +# Figure Friends Hips and Legs +11202p02c01: 11202p01c01 +11202p03c01: 11202p01c01 + +# Figure Friends Hips and Legs with Long Skirt +92249p02c01: 92249p01c01 + +# Figure Friends Hips and Legs with Layered Skirt +92250p02c01: 92250p01c01 +92250p03c01: 92250p01c01 +92250p04c01: 92250p01c01 +92250p05c01: 92250p01c01 +92250p06c01: 92250p01c01 +92250p07c01: 92250p01c01 +92250p08c01: 92250p01c01 + +# Figure Friends Hips and Legs with Cropped Trousers +92251p02c01: 92251p01c01 +92251p03c01: 92251p01c01 +92251p04c01: 92251p01c01 +92251p05c01: 92251p01c01 + +# Figure Friends Hips and Legs with Pleated Skirt +92252p02c01: 92252p01c01 +92252p03c01: 92252p01c01 +92252p04c01: 92252p01c01 +92252p05c01: 92252p01c01 +92252p06c01: 92252p01c01 +92252p07c01: 92252p01c01 + +# Figure Friends Hips and Legs with Trousers +92253p02c01: 92253p01c01 +92253p03c01: 92253p01c01 +92253p04c01: 92253p01c01 + +# Figure Friends Hips and Legs +15680p01c01: 15680c01 + +# Figure Friends Girl Torso with Arms +92241p02c01: 92241p01c01 +92241p03c01: 92241p01c01 +92241p04c01: 92241p01c01 +92241p05c01: 92241p01c01 +92241p06c01: 92241p01c01 +92241p07c01: 92241p01c01 +92241p08c01: 92241p01c01 +92241p09c01: 92241p01c01 +92241p11c01: 92241p01c01 +92241p12c01: 92241p01c01 +92241p13c01: 92241p01c01 +92241p14c01: 92241p01c01 +92241p15c01: 92241p01c01 +92241p16c01: 92241p01c01 +92241p17c01: 92241p01c01 +92241p19c01: 92241p01c01 + +# Figure Fabuland Pig 3 w Body +u9147p01c03: u9147p01c02 +u9147p01c04: u9147p01c02 +u9147p01c05: u9147p01c02 +u9147p01c06: u9147p01c02 +u9147p02c02: u9147p01c02 +u9147p03c02: u9147p01c02 + +# Figure Fabuland Elephant Head +u588p02c02: u588p01c02 + +30461c02: 30461c01 + +10350p01c01: 10350c01 +10350p02c01: 10350c01 + +# Minifig Headdress +30168a: 30168 +30168b: 30168 + +# Boat Section Stern 6 x 6 x 3.333 +164c02: 164c01 + +90391p02: 90391p01 + +4493c01: 4493c00 +4493c02: 4493c00 +4493c03: 4493c00 +4493c04: 4493c00 + +50231c01: 50231 +50231c02: 50231 +50231p01c01: 50231 + +56630c01: 56630 + +86038c01: 86038 + +99464c01: 99464 + +2453a: 2453 + +# Brick 1 x 1 with Orange Slices Pattern +3005bpf1: 3005 + +11146: 55805 + +4116604: 3009 + +56204: 45411 + +# Hinge Control Stick and Base +73587: 4592c01 +4296152: 4592c01 \ No newline at end of file diff --git a/app/Resources/relations/part_model.yml b/app/Resources/relations/part_model.yml new file mode 100644 index 0000000..2c68647 --- /dev/null +++ b/app/Resources/relations/part_model.yml @@ -0,0 +1,155 @@ +# Define the relationship between LDraw models that are not automatically recognizabe to eliminate duplication of models +# +# To load changes `run app:load:relation` command +# +# format: +# rebrickable_part_ID: ldraw_model_ID + +970d00: 970c00 +970d03: 970c00 +970d06: 970c00 +970d08: 970c00 +970d10: 970c00 +970d14: 970c00 +970d18: 970c00 +970d19: 970c00 +970d29: 970c00 +970d30: 970c00 +970x021: 970c00 +970d01: 3815c36 +970d05: 3815c36 +970d09: 3815c36 +970d12: 3815c36 + +57503: 70501a + +90391: 90391p01 + +2453a: 2453 +2454a: 2454 +2454b: 2454 + +4493cx6: 4493c00 + +298c02: 4592c01 +298c03: 4592c01 +298c04: 4592c01 +298c05: 4592c01 + +73983: 2429c01 + +# Tubes +75c03: 76263 +75c04: 76250 +75c05: 76307 +75c06: 76279 +75c07: 76289 +75c08: 76260 +75c09: 76324 +75c10: 76348 +75c11: 71505 +75c12: 71175 +75c13: 71551 +75c14: 71177 +75c15: 71194 +75c16: 71192 +75c17: 76270 +75c18: 71582 +75c19: 22463 +75c20: 76276 +75c21: 70978 +75c22: 76252 +75c23: 76254 +75c24: 76277 +75c26: 53475 +75c28: 76280 +75c29: 76389 +75c30: 76282 +75c31: 76283 +75c32: 57274 +75c33: 57274 +75c34: 22461 +75c40: 46305 +75c45: 76281 +75c53: 22296 + +78c02: 72504 +78c03: 72706 +78c04: 71952 +78c06: 71944 +78c08: 71951 +78c11: 71986 +78c19: 43675 + + +30361c: 30361d +30361b: 30361d +3048c: 3048 +98560: 3684c +10119: 51704 +3149: 3149d +75998: 4493c00 +10119: 51704 +30658: 3404 + +59275: 2599 +3008a03: 925 + +# Part molds fetched from rebrickable +73590c01a: 73590a +3847a: 3847 +44301a: 44301 +44567a: 44567 +44302a: 44302 +3001a: 3001 +3002a: 3002 +3947a: 3947 +30187a: 30187 +3046a: 3046 +18868a: 18868 +45707a: 45707 +2635a: 2635 +2714a: 2714 +73590c02a: 73590b +wheel2a: 568c01 +6014a: 6014 +18979a: 18978b +132a: u9131 +16816pr0001a: u9209p01 +57909a: 57909 +9244a: 575c01 +32005a: 2739a +2850a: 2850 +6048a: 6048 +30151a: 30151 +3035a: u8202 +30350a: 30350 +4856a: 4856 +6153a: 6153 +60583a: 60583 +30390a: 30390 +30237a: 30237 +4739a: 4739 + +4476b: 4476 +30237b: 95820 +44301b: 44301 +44302b: 44302 +32005b: 32005 +61927b: 61927 +3852b: 3852 +6228b: 6228 +18979b: 18979a +3634b: 574 +6014b: 6014 +3940b: 3940 +58123b: 58123p01 +6216b: 6216m + +32064c: 32064a +3062c: u9026 + +40344c01: 43123 +11895pr0001c01: 11895 +92456pr0021c01: 92241p03c01 + diff --git a/app/config/service/loader.yml b/app/config/service/loader.yml index e0c1e0a..61fde37 100644 --- a/app/config/service/loader.yml +++ b/app/config/service/loader.yml @@ -22,3 +22,8 @@ services: class: AppBundle\Service\Loader\LDrawLoaderService arguments: ['@service.ldview', '%ldraw_url%', '@manager.ldraw', '@util.dat.parser'] parent: service.loader + + service.loader.relation: + class: AppBundle\Service\Loader\RelationLoader + arguments: ['@manager.ldraw.model', '@repository.rebrickable.part', '@api.manager.rebrickable'] + parent: service.loader \ No newline at end of file diff --git a/app/config/service/service.yml b/app/config/service/service.yml index 2058bf8..ba38c4a 100644 --- a/app/config/service/service.yml +++ b/app/config/service/service.yml @@ -13,4 +13,8 @@ services: class: AppBundle\Form\FilterSetType arguments: ['@manager.brickset'] tags: - - { name: form.type } \ No newline at end of file + - { name: form.type } app.relation.mapper: + app.relation.mapper: + class: AppBundle\Utils\RelationMapper + arguments: + - ['%kernel.root_dir%/Resources/relations'] \ No newline at end of file diff --git a/src/AppBundle/Command/LoadRelationCommand.php b/src/AppBundle/Command/LoadRelationCommand.php new file mode 100644 index 0000000..bffaaf8 --- /dev/null +++ b/src/AppBundle/Command/LoadRelationCommand.php @@ -0,0 +1,27 @@ +setName('app:load:relations') + ->setDescription('Loads relations between LDraw models and Rebrickable parts') + ->setHelp('This command allows you to..'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $relationLoader = $this->getContainer()->get('service.loader.relation'); + $relationLoader->setOutput($output); + + //TODO log errors + $relationLoader->loadNotPaired(); + } +} diff --git a/src/AppBundle/Entity/Rebrickable/Part.php b/src/AppBundle/Entity/Rebrickable/Part.php index 33ee4c6..686a77a 100644 --- a/src/AppBundle/Entity/Rebrickable/Part.php +++ b/src/AppBundle/Entity/Rebrickable/Part.php @@ -2,6 +2,7 @@ namespace AppBundle\Entity\Rebrickable; +use AppBundle\Entity\LDraw\Model; use AppBundle\Entity\Traits\NameTrait; use AppBundle\Entity\Traits\NumberTrait; use Doctrine\Common\Collections\ArrayCollection; @@ -33,6 +34,13 @@ class Part */ protected $inventoryParts; + /** + * @var Model + * + * @ORM\ManyToOne(targetEntity="AppBundle\Entity\LDraw\Model", inversedBy="parts") + */ + private $model; + /** * Part constructor. */ @@ -92,4 +100,24 @@ class Part return $this; } + + /** + * @return Model + */ + public function getModel() + { + return $this->model; + } + + /** + * @param Model $model + * + * @return Part + */ + public function setModel($model) + { + $this->model = $model; + + return $this; + } } diff --git a/src/AppBundle/Repository/Rebrickable/PartRepository.php b/src/AppBundle/Repository/Rebrickable/PartRepository.php index 5bcacc1..c4adac1 100644 --- a/src/AppBundle/Repository/Rebrickable/PartRepository.php +++ b/src/AppBundle/Repository/Rebrickable/PartRepository.php @@ -2,6 +2,9 @@ namespace AppBundle\Repository\Rebrickable; +use AppBundle\Entity\LDraw\Model; +use AppBundle\Entity\LDraw\Part; +use AppBundle\Entity\Rebrickable\Category; use AppBundle\Entity\Rebrickable\Inventory; use AppBundle\Entity\Rebrickable\Inventory_Part; use AppBundle\Entity\Rebrickable\Set; @@ -10,6 +13,29 @@ use Doctrine\ORM\Query\Expr\Join; class PartRepository extends BaseRepository { + public function findAllByModel(Model $model) + { + $queryBuilder = $this->createQueryBuilder('part'); + + $queryBuilder + ->where('part.model = :model') + ->setParameter('model', $model); + + return $queryBuilder->getQuery()->getResult(); + } + + public function findAllNotPaired() + { + $queryBuilder = $this->createQueryBuilder('part') + ->leftJoin(Category::class, 'category', JOIN::WITH, 'part.category = category.id') + ->where('category.name NOT LIKE :categoryName') + ->andWhere('part.model IS NULL') + ->setParameter('categoryName', 'Non-LEGO') + ->distinct(true); + + return $queryBuilder->getQuery()->getResult(); + } + public function findAllBySetNumber($number) { $queryBuilder = $this->createQueryBuilder('part'); diff --git a/src/AppBundle/Repository/Rebrickable/SetRepository.php b/src/AppBundle/Repository/Rebrickable/SetRepository.php index 8a810e9..2237e9c 100644 --- a/src/AppBundle/Repository/Rebrickable/SetRepository.php +++ b/src/AppBundle/Repository/Rebrickable/SetRepository.php @@ -2,6 +2,7 @@ namespace AppBundle\Repository\Rebrickable; +use AppBundle\Entity\LDraw\Model; use AppBundle\Entity\Rebrickable\Inventory; use AppBundle\Entity\Rebrickable\Inventory_Part; use AppBundle\Entity\Rebrickable\Part; @@ -19,7 +20,22 @@ class SetRepository extends BaseRepository ->join(Inventory_Part::class, 'inventory_part', JOIN::WITH, 'inventory.id = inventory_part.inventory') ->join(Part::class, 'part', Join::WITH, 'inventory_part.part = part.number') ->where('part.number LIKE :number') - ->setParameter('number', $number.'%') + ->setParameter('number', $number) + ->distinct(true); + + return $queryBuilder->getQuery()->getResult(); + } + + public function findAllByModel(Model $model) + { + $queryBuilder = $this->createQueryBuilder('s'); + + $queryBuilder + ->join(Inventory::class, 'inventory', JOIN::WITH, 'inventory.set = s.number') + ->join(Inventory_Part::class, 'inventory_part', JOIN::WITH, 'inventory.id = inventory_part.inventory') + ->join(Part::class, 'part', Join::WITH, 'inventory_part.part = part.number') + ->where('part.model = :model') + ->setParameter('model', $model->getNumber()) ->distinct(true); return $queryBuilder->getQuery()->getResult(); diff --git a/src/AppBundle/Service/Loader/RelationLoader.php b/src/AppBundle/Service/Loader/RelationLoader.php new file mode 100644 index 0000000..d3be510 --- /dev/null +++ b/src/AppBundle/Service/Loader/RelationLoader.php @@ -0,0 +1,119 @@ +modelManager = $modelManager; + $this->partRepository = $partRepository; + $this->rebrickableAPIManager = $rebrickableApiManager; + } + + + /** + * + */ + public function loadAll() + { + $parts = $this->partRepository->findAll(); + + $this->initProgressBar(count($parts)); + /** @var Part $part */ + foreach ($parts as $part) { + $this->load($part); + + $this->progressBar->advance(); + } + $this->progressBar->finish(); + } + + /** + * + */ + public function loadNotPaired() + { + $parts = $this->partRepository->findAllNotPaired(); + + $this->initProgressBar(count($parts)); + /** @var Part $part */ + foreach ($parts as $part) { + $this->load($part); + + $this->progressBar->advance(); + } + $this->progressBar->finish(); + } + + /** + * Loads relations between Rebrickable part and ldraw models for $parts + * + * @param Part $part + * + * @return Model $m + */ + private function load($part) + { + $number = $part->getNumber(); + $model = $this->modelManager->findByNumber($number); + if (!$model) { + $number = $this->relationMapper->find($this->getPrintedParentId($number), 'part_model'); + $model = $this->modelManager->findByNumber($number); + + if (!$model) { + $model = $this->modelManager->findByName($part->getName()); + } + } + + if ($model) { + $part->setModel($model); + $this->partRepository->save($part); + } + } + + /** + * Get printed part parent number. + * + * @param $id + * + * @return string|null LDraw number of printed part parent + */ + private function getPrintedParentId($number) + { + if (preg_match('/(^970[c,x])([0-9a-z]*)$/', $number, $matches)) { + return '970c00'; + } elseif (preg_match('/(^973)([c,p][0-9a-z]*)$/', $number, $matches)) { + return '973c00'; + } elseif (preg_match('/(^.*)((pr[x]{0,1}[0-9]{1,7}[a-z]{0,1})|(pat[[0-9]{1,4}[a-z]{0,1}))$/', $number, $matches)) { + return $matches[1]; + } elseif (preg_match('/(^.*)((pb[0-9]{1,4}[a-z]{0,1}))$/', $number, $matches)) { + return $matches[1]; + } elseif (preg_match('/(^.*)(p[x]{0,1}[0-9a-z]{2,4})$/', $number, $matches)) { + return $matches[1]; + } + + return $number; + } +} diff --git a/src/AppBundle/Utils/RelationMapper.php b/src/AppBundle/Utils/RelationMapper.php new file mode 100644 index 0000000..257a8d4 --- /dev/null +++ b/src/AppBundle/Utils/RelationMapper.php @@ -0,0 +1,66 @@ +files()->name('*.yml')->in($resourcesDir); + foreach ($files as $file) { + $domain = substr($file->getFilename(), 0, -1 * strlen('yml') - 1); + $this->loadResource($file, $domain); + } + } + + /** + * Finds related part/model number to given $number in $domain resource or returns original $number if not found. + * + * @param string $number The part/model number + * @param string $domain The domain of relation type + * + * @throws InvalidArgumentException If the domain not found + * + * @return string The mapped string + */ + public function find($number, $domain) + { + if (isset($this->relations[$domain])) { + return isset($this->relations[$domain][$number]) ? $this->relations[$domain][$number] : $number; + } + throw new InvalidOptionsException(); + } + + /** + * Adds a Resource. + * + * @param $file + * @param $domain + */ + private function loadResource($file, $domain) + { + try { + $this->relations[$domain] = Yaml::parse(file_get_contents($file->getPathname())); + } catch (ParseException $e) { + throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s"', $file->getPathname()), 0, $e); + } + } +}