1<?php
2
3namespace Davaxi;
4
5use Davaxi\Sparkline\DataTrait;
6use Davaxi\Sparkline\FormatTrait;
7use Davaxi\Sparkline\Picture;
8use Davaxi\Sparkline\PointTrait;
9use Davaxi\Sparkline\StyleTrait;
10use InvalidArgumentException;
11
12/**
13 * Class Sparkline.
14 */
15class Sparkline
16{
17    use StyleTrait;
18    use DataTrait;
19    use FormatTrait;
20    use PointTrait;
21
22    const MIN_DATA_LENGTH = 2;
23    const FORMAT_DIMENSION = 2;
24    const HEXADECIMAL_ALIAS_LENGTH = 3;
25    const CSS_PADDING_ONE = 1;
26    const CSS_PADDING_TWO = 2;
27    const CSS_PADDING_THREE = 3;
28    const CSS_PADDING = 4;
29
30    /**
31     * @var string
32     *             ex: QUERY_STRING if dedicated url
33     */
34    protected $eTag;
35
36    /**
37     * @var int
38     */
39    protected $expire;
40
41    /**
42     * @var string
43     */
44    protected $filename = 'sparkline';
45
46    /**
47     * @var resource
48     */
49    protected $file;
50
51    /**
52     * @var array
53     */
54    protected $server = [];
55
56    /**
57     * Sparkline constructor.
58     *
59     * @codeCoverageIgnore
60     */
61    public function __construct()
62    {
63        if (!extension_loaded('gd')) {
64            throw new InvalidArgumentException('GD extension is not installed');
65        }
66    }
67
68    /**
69     * @param string|null $eTag
70     */
71    public function setETag($eTag)
72    {
73        if (null === $eTag) {
74            $this->eTag = null;
75
76            return;
77        }
78        $this->eTag = md5($eTag);
79    }
80
81    /**
82     * @param string $filename
83     *                         Without extension
84     */
85    public function setFilename(string $filename)
86    {
87        $this->filename = $filename;
88    }
89
90    /**
91     * @param string|int $expire
92     *                           time format or string format
93     */
94    public function setExpire($expire)
95    {
96        if (null === $expire) {
97            $this->expire = null;
98
99            return;
100        }
101        if (is_numeric($expire)) {
102            $this->expire = $expire;
103
104            return;
105        }
106        $this->expire = strtotime($expire);
107    }
108
109    public function generate()
110    {
111        list($width, $height) = $this->getNormalizedSize();
112
113        $count = $this->getCount();
114
115        $picture = new Picture($width, $height);
116        $picture->applyBackground($this->backgroundColor);
117
118        $lineThickness = (int)round($this->lineThickness * $this->ratioComputing);
119        $picture->applyThickness($lineThickness);
120
121        $stepCount = $this->getMaxNumberOfDataPointsAcrossSerieses();
122
123        foreach ($this->data as $seriesIndex => $series) {
124            $seriesNormalized = $this->getNormalizedData($seriesIndex);
125            list($polygon, $line) = $this->getChartElements($seriesNormalized, $stepCount);
126            $picture->applyPolygon($polygon, $this->getFillColor($seriesIndex), $count);
127            $picture->applyLine($line, $this->getLineColor($seriesIndex));
128
129            foreach ($this->points as $point) {
130                if ($point['series'] != $seriesIndex) {
131                    continue;
132                }
133
134                $isFirst = $point['index'] === 0;
135                $lineIndex = $isFirst ? 0 : $point['index'] - 1;
136                $picture->applyDot(
137                    $line[$lineIndex][$isFirst ? 0 : 2],
138                    $line[$lineIndex][$isFirst ? 1 : 3],
139                    $point['radius'] * $this->ratioComputing,
140                    $point['color']
141                );
142            }
143        }
144
145        $this->file = $picture->generate($this->width, $this->height);
146    }
147
148    /**
149     * @param array $server
150     */
151    public function setServer(array $server)
152    {
153        $this->server = $server;
154    }
155
156    /**
157     * @param string $key
158     *
159     * @return mixed|null
160     */
161    public function getServerValue(string $key)
162    {
163        if (isset($this->server[$key])) {
164            return $this->server[$key];
165        }
166
167        return null;
168    }
169
170    /**
171     * @return bool
172     */
173    protected function checkNoModified(): bool
174    {
175        $httpIfNoneMatch = $this->getServerValue('HTTP_IF_NONE_MATCH');
176        if ($this->eTag && $httpIfNoneMatch) {
177            if ($httpIfNoneMatch === $this->eTag) {
178                $serverProtocol = $this->getServerValue('SERVER_PROTOCOL');
179                header($serverProtocol . ' 304 Not Modified', true, 304);
180
181                return true;
182            }
183        }
184
185        return false;
186    }
187
188    public function display()
189    {
190        if (!$this->file) {
191            $this->generate();
192        }
193
194        if ($this->checkNoModified()) {
195            return;
196        }
197
198        header('Content-Type: image/png');
199        header('Content-Disposition: inline; filename="' . $this->filename . '.png"');
200        header('Accept-Ranges: none');
201        if ($this->eTag) {
202            header('ETag: ' . $this->eTag);
203        }
204        if (null !== $this->expire) {
205            header('Expires: ' . gmdate('D, d M Y H:i:s T', $this->expire));
206        }
207        imagepng($this->file);
208    }
209
210    /**
211     * @param string $savePath
212     */
213    public function save(string $savePath)
214    {
215        if (!$this->file) {
216            $this->generate();
217        }
218        imagepng($this->file, $savePath);
219    }
220
221    /**
222     * @return string
223     */
224    public function toBase64(): string
225    {
226        if (!$this->file) {
227            $this->generate();
228        }
229        ob_start();
230        imagepng($this->file);
231        $buffer = ob_get_contents();
232        if (ob_get_length()) {
233            ob_end_clean();
234        }
235
236        return base64_encode($buffer);
237    }
238
239    public function destroy()
240    {
241        if ($this->file) {
242            imagedestroy($this->file);
243        }
244        $this->file = null;
245    }
246}
247