Completed
Push — master ( 7f5809...3e2944 )
by Andreas
07:09
created

src/Control/WebMapTileService.php (4 issues)

1
<?php
2
3
namespace Smindel\GIS\Control;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Security\Security;
8
use SilverStripe\Security\Permission;
9
use SilverStripe\ORM\DB;
10
use Smindel\GIS\GIS;
11
use Smindel\GIS\Service\Tile;
12
13
class WebMapTileService extends AbstractGISWebServiceController
14
{
15
    private static $url_handlers = array(
16
        '$Model//$z/$x/$y' => 'handleAction',
17
    );
18
19
    /**
20
     * Buffer in pixel by wich the tile box is enlarged, which is used for
21
     * filtering the data. That's in orderer to include points from a bordering
22
     * tile that was cut and that wouldn't be rendered in this tile in order to
23
     * show a complete point. Use int as a uniform buffer around the current tile
24
     * or a two values for left/right and top/bottom or four values for left, top,
25
     * right and bottom.
26
     *
27
     * @var mixed
28
     */
29
    private static $tile_buffer = 5;
30
31
    private static $tile_size = 256;
32
33
    private static $wrap_date = true;
34
35
    private static $cache_path = 'tile-cache';
36
37
    private static $cache_ttl = 0;
38
39
    private static $default_style = [
40
        'gd' => [
41
            'backgroundcolor' => [0, 0, 0, 127],
42
            'strokecolor' => [60, 60, 210, 0],
43
            'fillcolor' => [60, 60, 210, 80],
44
            'setthickness' => [2],
45
            'pointradius' => 5,
46
        ],
47
        'imagick' => [
48
            'StrokeOpacity' => 1,
49
            'StrokeWidth' => 2,
50
            'StrokeColor' => 'rgb(60,60,210)',
51
            'FillColor' => 'rgba(60,60,210,.25)',
52
            'PointRadius' => 5,
53
        ],
54
    ];
55
56 2
    public function index($request)
57
    {
58 2
        $model = $this->model = $this->getModel($request);
59 2
        $config = $this->getConfig($model);
60
61
        if (
62 2
            ($cache = $config['cache_ttl'] ? sha1(json_encode($request->getVars())) : false)
63 1
            && ($age = $this->cacheAge($cache)) !== false
64
            && $config['cache_ttl'] > $age
65
        ) {
66
            $response = $this->getResponse();
67
            $response->addHeader('Content-Type', 'image/png');
68
            $response->setBody($this->readCache($cache));
69
            return $response;
70
        }
71
72 2
        $renderer = Config::inst()->get($this->getModel($request), 'tile_renderer');
73 2
        $response = $this->$renderer($request);
74
75 2
        if ($cache && $response->getStatusCode() == 200) {
76 1
            $this->writeCache($cache, $response->getBody());
77
        }
78
79 2
        return $response;
80
    }
81
82 2
    public function vector_renderer($request)
83
    {
84 2
        $model = $this->model = $this->getModel($request);
85 2
        $config = $this->getConfig($model);
86 2
        $list = $this->getRecords($request);
87
88 2
        $z = $request->param('z');
89 2
        $x = $request->param('x');
90 2
        $y = $request->param('y');
91
92 2
        $tileSize = $config['tile_size'];
93 2
        $tile = Tile::create($z, $x, $y, $config['default_style'], $config['wrap_date'], $tileSize);
94
95 2
        list($lon1, $lat1) = Tile::zxy2lonlat($z, $x, $y);
96 2
        list($lon2, $lat2) = Tile::zxy2lonlat($z, $x + 1, $y + 1);
97
98 2
        $geometryField = $config['geometry_field'];
99
100 2
        $bufferSize = $config['tile_buffer'];
101 2
        if (!is_array($bufferSize)) {
102 2
            $bufferSize = array_fill(0, 4, $bufferSize);
103
        } elseif (count($bufferSize) == 2) {
104
            $bufferSize += $bufferSize;
105
        }
106
107
        $buffer = [
108 2
            ($lon2 - $lon1) / $tileSize * $bufferSize[0],
109 2
            ($lat2 - $lat1) / $tileSize * $bufferSize[1],
110 2
            ($lon2 - $lon1) / $tileSize * $bufferSize[2],
111 2
            ($lat2 - $lat1) / $tileSize * $bufferSize[3],
112
        ];
113
114
        $boxes = [[
115 2
            [$lon1 - $buffer[0], $lat1 - $buffer[1]],
116 2
            [$lon2 + $buffer[2], $lat1 - $buffer[1]],
117 2
            [$lon2 + $buffer[2], $lat2 + $buffer[3]],
118 2
            [$lon1 - $buffer[0], $lat2 + $buffer[3]],
119 2
            [$lon1 - $buffer[0], $lat1 - $buffer[1]],
120
        ]];
121
122
        $bounds = [
123 2
            'type' => 'Polygon',
124 2
            'srid' => 4326,
125 2
            'coordinates' => $boxes,
126
        ];
127
128 2
        $list = $list->filter(
129 2
            $geometryField . ':ST_Intersects',
130 2
            GIS::array_to_ewkt(
131 2
                GIS::reproject_array(
132 2
                    $bounds,
133 2
                    Config::inst()->get(GIS::class, 'default_srid')
134
                )
135
            )
136
        );
137
138 2
        if ($request->requestVar('debug')) {
139
            $tile->debug("$z, $x, $y, " . $list->count());
140
        }
141
142 2
        $response = $this->getResponse();
143 2
        $response->addHeader('Content-Type', $tile->getContentType());
144 2
        $response->setBody($tile->render($list));
145
146 2
        return $response;
147
    }
148
149
    public function raster_renderer($request)
150
    {
151
        $model = $this->model = $this->getModel($request);
152
        $config = $this->getConfig($model);
153
        $raster = singleton($model);
154
155
        $z = $request->param('z');
156
        $x = $request->param('x');
157
        $y = $request->param('y');
158
159
        list($lon1, $lat1) = Tile::zxy2lonlat($z, $x, $y);
160
        list($lon2, $lat2) = Tile::zxy2lonlat($z, $x + 1, $y + 1);
161
162
        list($x1, $y1) = ($srid = $raster->getSrid()) == 4326 ? [$lon1, $lat1] : GIS::reproject_array(['srid' => 4326, 'type' => 'Point', 'coordinates' => [$lon1, $lat1]], $srid)['coordinates'];
163
        list($x2, $y2) = ($srid = $raster->getSrid()) == 4326 ? [$lon2, $lat2] : GIS::reproject_array(['srid' => 4326, 'type' => 'Point', 'coordinates' => [$lon2, $lat2]], $srid)['coordinates'];
164
165
        $sfx = ($x2 - $x1) / $config['tile_size'];
166
        $sfy = ($y2 - $y1) / $config['tile_size'];
167
168
        DB::query("set bytea_output='escape'");
169
170
        $rasterDef = ($colormap = $raster->getColorMap())
0 ignored issues
show
The assignment to $colormap is dead and can be removed.
Loading history...
171
            ? sprintf('ST_ColorMap(%1$s, \'%2$s\')', $raster->getRasterColumn(), $raster->getColorMap())
172
            : $raster->getRasterColumn();
173
174
        $sql = sprintf('
175
            SELECT
176
                ST_AsPNG(%3$s) pngbinary
177
            FROM
178
                ST_Retile(
179
                    \'%1$s\'::regclass,
180
                    \'%2$s\',
181
                    ST_MakeEnvelope(%4$f, %5$f, %6$f, %7$f, %8$d),
182
                    %9$f, %10$f,
183
                    %11$d, %11$d
184
                ) %2$s
185
            LIMIT 1;
186
            ',
187
            $raster->getTableName(),
188
            $raster->getRasterColumn(),
189
            $rasterDef,
190
            $x1, $y1, $x2, $y2,
191
            $srid,
192
            $sfx, $sfy,
193
            $config['tile_size']
194
        );
195
196
        $query = DB::query($sql);
197
198
        $raw = $query->value();
199
        $response = $this->getResponse();
200
201
        $dimensions = $raster->getDimensions();
202
203
        $padding[0] = ($dimensions[0] - $x1) / $sfx;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$padding was never initialized. Although not strictly required by PHP, it is generally a good practice to add $padding = array(); before regardless.
Loading history...
204
        $padding[1] = ($dimensions[1] - $y1) / $sfy;
205
        $padding[2] = ($x2 - $dimensions[2]) / $sfx;
206
        $padding[3] = ($y2 - $dimensions[3]) / $sfy;
207
208
        $response->addHeader('Content-Type', 'image/png');
209
        if (!$raw) {
210
            $response->setBody(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII='));
211
        } else if (max(...$padding) > 0) {
212
213
            $imgout = imagecreatetruecolor($config['tile_size'], $config['tile_size']);
214
            imagecolortransparent($imgout, imagecolorallocatealpha($imgout, 0, 0, 0, 0));
215
            imagealphablending($imgout, true);
216
217
            imagecopyresampled(
218
                $imgout,
219
                imagecreatefromstring(pg_unescape_bytea($raw)),
220
                $padding[0] > 0 ? $padding[0] : 0,
221
                $padding[1] > 0 ? $padding[1] : 0,
222
                0,
223
                0,
224
                $config['tile_size'],
225
                $config['tile_size'],
226
                (max($x1, $x2) - min($x1, $x2)) / abs($sfx),
0 ignored issues
show
max($x1, $x2) - min($x1, $x2) / abs($sfx) of type double is incompatible with the type integer expected by parameter $src_w of imagecopyresampled(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
                /** @scrutinizer ignore-type */ (max($x1, $x2) - min($x1, $x2)) / abs($sfx),
Loading history...
227
                (max($y1, $y2) - min($y1, $y2)) / abs($sfy)
0 ignored issues
show
max($y1, $y2) - min($y1, $y2) / abs($sfy) of type double is incompatible with the type integer expected by parameter $src_h of imagecopyresampled(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
                /** @scrutinizer ignore-type */ (max($y1, $y2) - min($y1, $y2)) / abs($sfy)
Loading history...
228
            );
229
230
            ob_start();
231
            imagepng($imgout);
232
233
            $response->setBody(ob_get_clean());
234
        } else {
235
            $response->setBody(pg_unescape_bytea($raw));
236
        }
237
238
        DB::query("set bytea_output='hex'");
239
240
        return $response;
241
    }
242
243 1
    protected function cacheFile($cache)
244
    {
245 1
        $dir = $this->getConfig($this->model)['cache_path'] . DIRECTORY_SEPARATOR;
246 1
        $dir = $dir[0] != DIRECTORY_SEPARATOR
247 1
            ? TEMP_PATH . DIRECTORY_SEPARATOR . $dir
248 1
            : $dir;
249
250 1
        if (!file_exists($dir)) {
251 1
            mkdir($dir, fileperms(TEMP_PATH), true);
252
        }
253
254 1
        return $dir . $cache;
255
    }
256
257 1
    protected function cacheAge($cache)
258
    {
259 1
        return is_readable($file = $this->cacheFile($cache))
260
            ? time() - filemtime($file)
261 1
            : false;
262
    }
263
264
    protected function readCache($cache)
265
    {
266
        return file_get_contents($this->cacheFile($cache));
267
    }
268
269 1
    protected function writeCache($cache, $data)
270
    {
271 1
        file_put_contents($this->cacheFile($cache), $data);
272 1
    }
273
}
274