|
|
<?php |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace Zxing\Qrcode\Detector; |
|
|
|
|
|
use Zxing\BinaryBitmap; |
|
|
use Zxing\Common\BitMatrix; |
|
|
use Zxing\NotFoundException; |
|
|
use Zxing\ResultPoint; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FinderPatternFinder |
|
|
{ |
|
|
protected static $MIN_SKIP = 3; |
|
|
protected static $MAX_MODULES = 57; |
|
|
private static $CENTER_QUORUM = 2; |
|
|
private $image; |
|
|
private $average; |
|
|
private $possibleCenters; |
|
|
private $hasSkipped = false; |
|
|
private $crossCheckStateCount; |
|
|
private $resultPointCallback; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct($image, $resultPointCallback = null) |
|
|
{ |
|
|
$this->image = $image; |
|
|
|
|
|
|
|
|
$this->possibleCenters = []; |
|
|
$this->crossCheckStateCount = fill_array(0, 5, 0); |
|
|
$this->resultPointCallback = $resultPointCallback; |
|
|
} |
|
|
|
|
|
final public function find($hints) |
|
|
{ |
|
|
$tryHarder = true; |
|
|
$pureBarcode = $hints != null && $hints['PURE_BARCODE']; |
|
|
$maxI = $this->image->getHeight(); |
|
|
$maxJ = $this->image->getWidth(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$iSkip = (int)((3 * $maxI) / (4 * self::$MAX_MODULES)); |
|
|
if ($iSkip < self::$MIN_SKIP || $tryHarder) { |
|
|
$iSkip = self::$MIN_SKIP; |
|
|
} |
|
|
|
|
|
$done = false; |
|
|
$stateCount = []; |
|
|
for ($i = $iSkip - 1; $i < $maxI && !$done; $i += $iSkip) { |
|
|
|
|
|
$stateCount[0] = 0; |
|
|
$stateCount[1] = 0; |
|
|
$stateCount[2] = 0; |
|
|
$stateCount[3] = 0; |
|
|
$stateCount[4] = 0; |
|
|
$currentState = 0; |
|
|
for ($j = 0; $j < $maxJ; $j++) { |
|
|
if ($this->image->get($j, $i)) { |
|
|
|
|
|
if (($currentState & 1) == 1) { |
|
|
$currentState++; |
|
|
} |
|
|
$stateCount[$currentState]++; |
|
|
} else { |
|
|
if (($currentState & 1) == 0) { |
|
|
if ($currentState == 4) { |
|
|
if (self::foundPatternCross($stateCount)) { |
|
|
$confirmed = $this->handlePossibleCenter($stateCount, $i, $j, $pureBarcode); |
|
|
if ($confirmed) { |
|
|
|
|
|
|
|
|
$iSkip = 3; |
|
|
if ($this->hasSkipped) { |
|
|
$done = $this->haveMultiplyConfirmedCenters(); |
|
|
} else { |
|
|
$rowSkip = $this->findRowSkip(); |
|
|
if ($rowSkip > $stateCount[2]) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$i += $rowSkip - $stateCount[2] - $iSkip; |
|
|
$j = $maxJ - 1; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
$stateCount[0] = $stateCount[2]; |
|
|
$stateCount[1] = $stateCount[3]; |
|
|
$stateCount[2] = $stateCount[4]; |
|
|
$stateCount[3] = 1; |
|
|
$stateCount[4] = 0; |
|
|
$currentState = 3; |
|
|
continue; |
|
|
} |
|
|
|
|
|
$currentState = 0; |
|
|
$stateCount[0] = 0; |
|
|
$stateCount[1] = 0; |
|
|
$stateCount[2] = 0; |
|
|
$stateCount[3] = 0; |
|
|
$stateCount[4] = 0; |
|
|
} else { |
|
|
$stateCount[0] = $stateCount[2]; |
|
|
$stateCount[1] = $stateCount[3]; |
|
|
$stateCount[2] = $stateCount[4]; |
|
|
$stateCount[3] = 1; |
|
|
$stateCount[4] = 0; |
|
|
$currentState = 3; |
|
|
} |
|
|
} else { |
|
|
$stateCount[++$currentState]++; |
|
|
} |
|
|
} else { |
|
|
$stateCount[$currentState]++; |
|
|
} |
|
|
} |
|
|
} |
|
|
if (self::foundPatternCross($stateCount)) { |
|
|
$confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ, $pureBarcode); |
|
|
if ($confirmed) { |
|
|
$iSkip = $stateCount[0]; |
|
|
if ($this->hasSkipped) { |
|
|
|
|
|
$done = $this->haveMultiplyConfirmedCenters(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
$patternInfo = $this->selectBestPatterns(); |
|
|
$patternInfo = ResultPoint::orderBestPatterns($patternInfo); |
|
|
|
|
|
return new FinderPatternInfo($patternInfo); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected static function foundPatternCross($stateCount) |
|
|
{ |
|
|
$totalModuleSize = 0; |
|
|
for ($i = 0; $i < 5; $i++) { |
|
|
$count = $stateCount[$i]; |
|
|
if ($count == 0) { |
|
|
return false; |
|
|
} |
|
|
$totalModuleSize += $count; |
|
|
} |
|
|
if ($totalModuleSize < 7) { |
|
|
return false; |
|
|
} |
|
|
$moduleSize = $totalModuleSize / 7.0; |
|
|
$maxVariance = $moduleSize / 2.0; |
|
|
|
|
|
|
|
|
return |
|
|
abs($moduleSize - $stateCount[0]) < $maxVariance && |
|
|
abs($moduleSize - $stateCount[1]) < $maxVariance && |
|
|
abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance && |
|
|
abs($moduleSize - $stateCount[3]) < $maxVariance && |
|
|
abs($moduleSize - $stateCount[4]) < $maxVariance; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected final function handlePossibleCenter($stateCount, $i, $j, $pureBarcode) |
|
|
{ |
|
|
$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + |
|
|
$stateCount[4]; |
|
|
$centerJ = $this->centerFromEnd($stateCount, $j); |
|
|
$centerI = $this->crossCheckVertical($i, (int)($centerJ), $stateCount[2], $stateCountTotal); |
|
|
if (!is_nan($centerI)) { |
|
|
|
|
|
$centerJ = $this->crossCheckHorizontal((int)($centerJ), (int)($centerI), $stateCount[2], $stateCountTotal); |
|
|
if (!is_nan($centerJ) && |
|
|
(!$pureBarcode || $this->crossCheckDiagonal((int)($centerI), (int)($centerJ), $stateCount[2], $stateCountTotal)) |
|
|
) { |
|
|
$estimatedModuleSize = (float)$stateCountTotal / 7.0; |
|
|
$found = false; |
|
|
for ($index = 0; $index < count($this->possibleCenters); $index++) { |
|
|
$center = $this->possibleCenters[$index]; |
|
|
|
|
|
if ($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)) { |
|
|
$this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize); |
|
|
$found = true; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (!$found) { |
|
|
$point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize); |
|
|
$this->possibleCenters[] = $point; |
|
|
if ($this->resultPointCallback != null) { |
|
|
$this->resultPointCallback->foundPossibleResultPoint($point); |
|
|
} |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static function centerFromEnd($stateCount, $end) |
|
|
{ |
|
|
return (float)($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function crossCheckVertical($startI, $centerJ, $maxCount, |
|
|
$originalStateCountTotal) |
|
|
{ |
|
|
$image = $this->image; |
|
|
|
|
|
$maxI = $image->getHeight(); |
|
|
$stateCount = $this->getCrossCheckStateCount(); |
|
|
|
|
|
|
|
|
$i = $startI; |
|
|
while ($i >= 0 && $image->get($centerJ, $i)) { |
|
|
$stateCount[2]++; |
|
|
$i--; |
|
|
} |
|
|
if ($i < 0) { |
|
|
return NAN; |
|
|
} |
|
|
while ($i >= 0 && !$image->get($centerJ, $i) && $stateCount[1] <= $maxCount) { |
|
|
$stateCount[1]++; |
|
|
$i--; |
|
|
} |
|
|
|
|
|
if ($i < 0 || $stateCount[1] > $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
while ($i >= 0 && $image->get($centerJ, $i) && $stateCount[0] <= $maxCount) { |
|
|
$stateCount[0]++; |
|
|
$i--; |
|
|
} |
|
|
if ($stateCount[0] > $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
|
|
|
|
|
|
$i = $startI + 1; |
|
|
while ($i < $maxI && $image->get($centerJ, $i)) { |
|
|
$stateCount[2]++; |
|
|
$i++; |
|
|
} |
|
|
if ($i == $maxI) { |
|
|
return NAN; |
|
|
} |
|
|
while ($i < $maxI && !$image->get($centerJ, $i) && $stateCount[3] < $maxCount) { |
|
|
$stateCount[3]++; |
|
|
$i++; |
|
|
} |
|
|
if ($i == $maxI || $stateCount[3] >= $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
while ($i < $maxI && $image->get($centerJ, $i) && $stateCount[4] < $maxCount) { |
|
|
$stateCount[4]++; |
|
|
$i++; |
|
|
} |
|
|
if ($stateCount[4] >= $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + |
|
|
$stateCount[4]; |
|
|
if (5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal) { |
|
|
return NAN; |
|
|
} |
|
|
|
|
|
return self::foundPatternCross($stateCount) ? $this->centerFromEnd($stateCount, $i) : NAN; |
|
|
} |
|
|
|
|
|
private function getCrossCheckStateCount() |
|
|
{ |
|
|
$this->crossCheckStateCount[0] = 0; |
|
|
$this->crossCheckStateCount[1] = 0; |
|
|
$this->crossCheckStateCount[2] = 0; |
|
|
$this->crossCheckStateCount[3] = 0; |
|
|
$this->crossCheckStateCount[4] = 0; |
|
|
|
|
|
return $this->crossCheckStateCount; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function crossCheckHorizontal($startJ, $centerI, $maxCount, |
|
|
$originalStateCountTotal) |
|
|
{ |
|
|
$image = $this->image; |
|
|
|
|
|
$maxJ = $this->image->getWidth(); |
|
|
$stateCount = $this->getCrossCheckStateCount(); |
|
|
|
|
|
$j = $startJ; |
|
|
while ($j >= 0 && $image->get($j, $centerI)) { |
|
|
$stateCount[2]++; |
|
|
$j--; |
|
|
} |
|
|
if ($j < 0) { |
|
|
return NAN; |
|
|
} |
|
|
while ($j >= 0 && !$image->get($j, $centerI) && $stateCount[1] <= $maxCount) { |
|
|
$stateCount[1]++; |
|
|
$j--; |
|
|
} |
|
|
if ($j < 0 || $stateCount[1] > $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
while ($j >= 0 && $image->get($j, $centerI) && $stateCount[0] <= $maxCount) { |
|
|
$stateCount[0]++; |
|
|
$j--; |
|
|
} |
|
|
if ($stateCount[0] > $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
|
|
|
$j = $startJ + 1; |
|
|
while ($j < $maxJ && $image->get($j, $centerI)) { |
|
|
$stateCount[2]++; |
|
|
$j++; |
|
|
} |
|
|
if ($j == $maxJ) { |
|
|
return NAN; |
|
|
} |
|
|
while ($j < $maxJ && !$image->get($j, $centerI) && $stateCount[3] < $maxCount) { |
|
|
$stateCount[3]++; |
|
|
$j++; |
|
|
} |
|
|
if ($j == $maxJ || $stateCount[3] >= $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
while ($j < $maxJ && $this->image->get($j, $centerI) && $stateCount[4] < $maxCount) { |
|
|
$stateCount[4]++; |
|
|
$j++; |
|
|
} |
|
|
if ($stateCount[4] >= $maxCount) { |
|
|
return NAN; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + |
|
|
$stateCount[4]; |
|
|
if (5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal) { |
|
|
return NAN; |
|
|
} |
|
|
|
|
|
return $this->foundPatternCross($stateCount) ? $this->centerFromEnd($stateCount, $j) : NAN; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function crossCheckDiagonal($startI, $centerJ, $maxCount, $originalStateCountTotal) |
|
|
{ |
|
|
$stateCount = $this->getCrossCheckStateCount(); |
|
|
|
|
|
|
|
|
$i = 0; |
|
|
$startI = (int)($startI); |
|
|
$centerJ = (int)($centerJ); |
|
|
while ($startI >= $i && $centerJ >= $i && $this->image->get($centerJ - $i, $startI - $i)) { |
|
|
$stateCount[2]++; |
|
|
$i++; |
|
|
} |
|
|
|
|
|
if ($startI < $i || $centerJ < $i) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
while ($startI >= $i && $centerJ >= $i && !$this->image->get($centerJ - $i, $startI - $i) && |
|
|
$stateCount[1] <= $maxCount) { |
|
|
$stateCount[1]++; |
|
|
$i++; |
|
|
} |
|
|
|
|
|
|
|
|
if ($startI < $i || $centerJ < $i || $stateCount[1] > $maxCount) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
while ($startI >= $i && $centerJ >= $i && $this->image->get($centerJ - $i, $startI - $i) && |
|
|
$stateCount[0] <= $maxCount) { |
|
|
$stateCount[0]++; |
|
|
$i++; |
|
|
} |
|
|
if ($stateCount[0] > $maxCount) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
$maxI = $this->image->getHeight(); |
|
|
$maxJ = $this->image->getWidth(); |
|
|
|
|
|
|
|
|
$i = 1; |
|
|
while ($startI + $i < $maxI && $centerJ + $i < $maxJ && $this->image->get($centerJ + $i, $startI + $i)) { |
|
|
$stateCount[2]++; |
|
|
$i++; |
|
|
} |
|
|
|
|
|
|
|
|
if ($startI + $i >= $maxI || $centerJ + $i >= $maxJ) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
while ($startI + $i < $maxI && $centerJ + $i < $maxJ && !$this->image->get($centerJ + $i, $startI + $i) && |
|
|
$stateCount[3] < $maxCount) { |
|
|
$stateCount[3]++; |
|
|
$i++; |
|
|
} |
|
|
|
|
|
if ($startI + $i >= $maxI || $centerJ + $i >= $maxJ || $stateCount[3] >= $maxCount) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
while ($startI + $i < $maxI && $centerJ + $i < $maxJ && $this->image->get($centerJ + $i, $startI + $i) && |
|
|
$stateCount[4] < $maxCount) { |
|
|
$stateCount[4]++; |
|
|
$i++; |
|
|
} |
|
|
|
|
|
if ($stateCount[4] >= $maxCount) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]; |
|
|
|
|
|
return |
|
|
abs($stateCountTotal - $originalStateCountTotal) < 2 * $originalStateCountTotal && |
|
|
self::foundPatternCross($stateCount); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function haveMultiplyConfirmedCenters() |
|
|
{ |
|
|
$confirmedCount = 0; |
|
|
$totalModuleSize = 0.0; |
|
|
$max = count($this->possibleCenters); |
|
|
foreach ($this->possibleCenters as $pattern) { |
|
|
if ($pattern->getCount() >= self::$CENTER_QUORUM) { |
|
|
$confirmedCount++; |
|
|
$totalModuleSize += $pattern->getEstimatedModuleSize(); |
|
|
} |
|
|
} |
|
|
if ($confirmedCount < 3) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$average = $totalModuleSize / (float)$max; |
|
|
$totalDeviation = 0.0; |
|
|
foreach ($this->possibleCenters as $pattern) { |
|
|
$totalDeviation += abs($pattern->getEstimatedModuleSize() - $average); |
|
|
} |
|
|
|
|
|
return $totalDeviation <= 0.05 * $totalModuleSize; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function findRowSkip() |
|
|
{ |
|
|
$max = count($this->possibleCenters); |
|
|
if ($max <= 1) { |
|
|
return 0; |
|
|
} |
|
|
$firstConfirmedCenter = null; |
|
|
foreach ($this->possibleCenters as $center) { |
|
|
|
|
|
|
|
|
if ($center->getCount() >= self::$CENTER_QUORUM) { |
|
|
if ($firstConfirmedCenter == null) { |
|
|
$firstConfirmedCenter = $center; |
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$this->hasSkipped = true; |
|
|
|
|
|
return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) - |
|
|
abs($firstConfirmedCenter->getY() - $center->getY())) / 2); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function selectBestPatterns() |
|
|
{ |
|
|
$startSize = count($this->possibleCenters); |
|
|
if ($startSize < 3) { |
|
|
|
|
|
throw new NotFoundException; |
|
|
} |
|
|
|
|
|
|
|
|
if ($startSize > 3) { |
|
|
|
|
|
$totalModuleSize = 0.0; |
|
|
$square = 0.0; |
|
|
foreach ($this->possibleCenters as $center) { |
|
|
$size = $center->getEstimatedModuleSize(); |
|
|
$totalModuleSize += $size; |
|
|
$square += $size * $size; |
|
|
} |
|
|
$this->average = $totalModuleSize / (float)$startSize; |
|
|
$stdDev = (float)sqrt($square / $startSize - $this->average * $this->average); |
|
|
|
|
|
usort($this->possibleCenters, [$this, 'FurthestFromAverageComparator']); |
|
|
|
|
|
$limit = max(0.2 * $this->average, $stdDev); |
|
|
|
|
|
for ($i = 0; $i < count($this->possibleCenters) && count($this->possibleCenters) > 3; $i++) { |
|
|
$pattern = $this->possibleCenters[$i]; |
|
|
if (abs($pattern->getEstimatedModuleSize() - $this->average) > $limit) { |
|
|
unset($this->possibleCenters[$i]); |
|
|
$this->possibleCenters = array_values($this->possibleCenters); |
|
|
$i--; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (count($this->possibleCenters) > 3) { |
|
|
|
|
|
|
|
|
$totalModuleSize = 0.0; |
|
|
foreach ($this->possibleCenters as $possibleCenter) { |
|
|
$totalModuleSize += $possibleCenter->getEstimatedModuleSize(); |
|
|
} |
|
|
|
|
|
$this->average = $totalModuleSize / (float)count($this->possibleCenters); |
|
|
|
|
|
usort($this->possibleCenters, [$this, 'CenterComparator']); |
|
|
|
|
|
array_slice($this->possibleCenters, 3, count($this->possibleCenters) - 3); |
|
|
} |
|
|
|
|
|
return [$this->possibleCenters[0], $this->possibleCenters[1], $this->possibleCenters[2]]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function FurthestFromAverageComparator($center1, $center2) |
|
|
{ |
|
|
|
|
|
$dA = abs($center2->getEstimatedModuleSize() - $this->average); |
|
|
$dB = abs($center1->getEstimatedModuleSize() - $this->average); |
|
|
if ($dA < $dB) { |
|
|
return -1; |
|
|
} elseif ($dA == $dB) { |
|
|
return 0; |
|
|
} else { |
|
|
return 1; |
|
|
} |
|
|
} |
|
|
|
|
|
public function CenterComparator($center1, $center2) |
|
|
{ |
|
|
if ($center2->getCount() == $center1->getCount()) { |
|
|
$dA = abs($center2->getEstimatedModuleSize() - $this->average); |
|
|
$dB = abs($center1->getEstimatedModuleSize() - $this->average); |
|
|
if ($dA < $dB) { |
|
|
return 1; |
|
|
} elseif ($dA == $dB) { |
|
|
return 0; |
|
|
} else { |
|
|
return -1; |
|
|
} |
|
|
} else { |
|
|
return $center2->getCount() - $center1->getCount(); |
|
|
} |
|
|
} |
|
|
|
|
|
protected final function getImage() |
|
|
{ |
|
|
return $this->image; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected final function getPossibleCenters() |
|
|
{ |
|
|
return $this->possibleCenters; |
|
|
} |
|
|
} |
|
|
|