QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
qgstiles.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstiles.cpp
3 --------------------------------------
4 Date : March 2020
5 Copyright : (C) 2020 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgstiles.h"
17
18#include "qgslogger.h"
20#include "qgsrendercontext.h"
21#include "qgsunittypes.h"
22
24{
25 constexpr double z0xMin = -20037508.3427892;
26 constexpr double z0yMax = 20037508.3427892;
27
28 return fromCustomDef( zoomLevel, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ), QgsPointXY( z0xMin, z0yMax ), 2 * z0yMax );
29}
30
32 const QgsPointXY &z0TopLeftPoint, double z0Dimension, int z0MatrixWidth, int z0MatrixHeight )
33{
34 // Square extent calculation
35 double z0xMin = z0TopLeftPoint.x();
36 double z0yMax = z0TopLeftPoint.y();
37 double z0xMax = z0xMin + z0MatrixWidth * z0Dimension;
38 double z0yMin = z0yMax - z0MatrixHeight * z0Dimension;
39
40 // Constant for scale denominator calculation
41 constexpr double TILE_SIZE = 256.0;
42 constexpr double PIXELS_TO_M = 2.8 / 10000.0; // WMS/WMTS define "standardized rendering pixel size" as 0.28mm
43 const double unitToMeters = QgsUnitTypes::fromUnitToUnitFactor( crs.mapUnits(), Qgis::DistanceUnit::Meters );
44 // Scale denominator calculation
45 const double scaleDenom0 = ( z0Dimension / TILE_SIZE ) * ( unitToMeters / PIXELS_TO_M );
46
47 int numTiles = static_cast<int>( pow( 2, zoomLevel ) ); // assuming we won't ever go over 30 zoom levels
48
50 tm.mCrs = crs;
51 tm.mZoomLevel = zoomLevel;
52 tm.mMatrixWidth = z0MatrixWidth * numTiles;
53 tm.mMatrixHeight = z0MatrixHeight * numTiles;
54 tm.mTileXSpan = ( z0xMax - z0xMin ) / tm.mMatrixWidth;
55 tm.mTileYSpan = ( z0yMax - z0yMin ) / tm.mMatrixHeight;
56 tm.mExtent = QgsRectangle( z0xMin, z0yMin, z0xMax, z0yMax );
57 tm.mScaleDenom = scaleDenom0 / pow( 2, zoomLevel );
58 return tm;
59}
60
61QgsTileMatrix QgsTileMatrix::fromTileMatrix( const int zoomLevel, const QgsTileMatrix &tileMatrix )
62{
64 int numTiles = static_cast<int>( pow( 2, zoomLevel ) ); // assuming we won't ever go over 30 zoom levels
65 int aZoomLevel = tileMatrix.zoomLevel();
66 int aNumTiles = static_cast<int>( pow( 2, aZoomLevel ) );
67 int aMatrixWidth = tileMatrix.matrixWidth();
68 int aMatrixHeight = tileMatrix.matrixHeight();
69 QgsRectangle aExtent = tileMatrix.extent();
70 tm.mCrs = tileMatrix.crs();
71 tm.mZoomLevel = zoomLevel;
72 tm.mMatrixWidth = aMatrixWidth * numTiles / aNumTiles;
73 tm.mMatrixHeight = aMatrixHeight * numTiles / aNumTiles;
74 tm.mTileXSpan = aExtent.width() / tm.mMatrixWidth;
75 tm.mTileYSpan = aExtent.height() / tm.mMatrixHeight;
76 tm.mExtent = aExtent;
77 tm.mScaleDenom = tileMatrix.scale() * pow( 2, aZoomLevel ) / pow( 2, zoomLevel );
78 return tm;
79}
80
82{
83 double xMin = mExtent.xMinimum() + mTileXSpan * id.column();
84 double xMax = xMin + mTileXSpan;
85 double yMax = mExtent.yMaximum() - mTileYSpan * id.row();
86 double yMin = yMax - mTileYSpan;
87 return QgsRectangle( xMin, yMin, xMax, yMax );
88}
89
91{
92 double x = mExtent.xMinimum() + mTileXSpan * id.column() + mTileXSpan / 2;
93 double y = mExtent.yMaximum() - mTileYSpan * id.row() - mTileYSpan / 2;
94 return QgsPointXY( x, y );
95}
96
98{
99 double x0 = std::clamp( r.xMinimum(), mExtent.xMinimum(), mExtent.xMaximum() );
100 double y0 = std::clamp( r.yMinimum(), mExtent.yMinimum(), mExtent.yMaximum() );
101 double x1 = std::clamp( r.xMaximum(), mExtent.xMinimum(), mExtent.xMaximum() );
102 double y1 = std::clamp( r.yMaximum(), mExtent.yMinimum(), mExtent.yMaximum() );
103 if ( x0 >= x1 || y0 >= y1 )
104 return QgsTileRange(); // nothing to display
105
106 double tileX1 = ( x0 - mExtent.xMinimum() ) / mTileXSpan;
107 double tileX2 = ( x1 - mExtent.xMinimum() ) / mTileXSpan;
108 double tileY1 = ( mExtent.yMaximum() - y1 ) / mTileYSpan;
109 double tileY2 = ( mExtent.yMaximum() - y0 ) / mTileYSpan;
110
111 QgsDebugMsgLevel( QStringLiteral( "Tile range of edges [%1,%2] - [%3,%4]" ).arg( tileX1 ).arg( tileY1 ).arg( tileX2 ).arg( tileY2 ), 2 );
112
113 // figure out tile range from zoom
114 int startColumn = std::clamp( static_cast<int>( floor( tileX1 ) ), 0, mMatrixWidth - 1 );
115 int endColumn = std::clamp( static_cast<int>( floor( tileX2 ) ), 0, mMatrixWidth - 1 );
116 int startRow = std::clamp( static_cast<int>( floor( tileY1 ) ), 0, mMatrixHeight - 1 );
117 int endRow = std::clamp( static_cast<int>( floor( tileY2 ) ), 0, mMatrixHeight - 1 );
118 return QgsTileRange( startColumn, endColumn, startRow, endRow );
119}
120
121QPointF QgsTileMatrix::mapToTileCoordinates( const QgsPointXY &mapPoint ) const
122{
123 double dx = mapPoint.x() - mExtent.xMinimum();
124 double dy = mExtent.yMaximum() - mapPoint.y();
125 return QPointF( dx / mTileXSpan, dy / mTileYSpan );
126}
127
128//
129// QgsTileMatrixSet
130//
131
133{
134 return mTileMatrices.isEmpty();
135}
136
137void QgsTileMatrixSet::addGoogleCrs84QuadTiles( int minimumZoom, int maximumZoom )
138{
139 if ( maximumZoom < minimumZoom )
140 std::swap( minimumZoom, maximumZoom );
141
142 for ( int zoom = minimumZoom; zoom <= maximumZoom; ++zoom )
143 {
145 }
146
147 mRootMatrix = QgsTileMatrix::fromWebMercator( 0 );
148}
149
151{
152 return mTileMatrices.value( zoom );
153}
154
156{
157 return mRootMatrix;
158}
159
161{
162 mRootMatrix = matrix;
163}
164
166{
167 mTileMatrices.insert( matrix.zoomLevel(), matrix );
168}
169
171{
172 int res = -1;
173 for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
174 {
175 if ( res == -1 || it->zoomLevel() < res )
176 res = it->zoomLevel();
177 }
178 return res;
179}
180
182{
183 int res = -1;
184 for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
185 {
186 if ( res == -1 || it->zoomLevel() > res )
187 res = it->zoomLevel();
188 }
189 return res;
190}
191
192void QgsTileMatrixSet::dropMatricesOutsideZoomRange( int minimumZoom, int maximumZoom )
193{
194 for ( auto it = mTileMatrices.begin(); it != mTileMatrices.end(); )
195 {
196 if ( it->zoomLevel() < minimumZoom || it->zoomLevel() > maximumZoom )
197 {
198 it = mTileMatrices.erase( it );
199 }
200 else
201 {
202 ++it;
203 }
204 }
205}
206
208{
209 if ( mTileMatrices.empty() )
211
212 return mTileMatrices.value( minimumZoom() ).crs();
213}
214
215double QgsTileMatrixSet::scaleToZoom( double scale ) const
216{
217 int zoomUnder = -1;
218 int zoomOver = -1;
219 double scaleUnder = 0;
220 double scaleOver = 0;
221
222 switch ( mScaleToTileZoomMethod )
223 {
225 {
226 // TODO: it seems that map scale is double (is that because of high-dpi screen?)
227 // (this TODO was taken straight from QgsVectorTileUtils::scaleToZoom!)
228 scale *= 2;
229 break;
230 }
232 break;
233 }
234
235 for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
236 {
237 if ( it->scale() > scale && ( zoomUnder == -1 || zoomUnder < it->zoomLevel() ) )
238 {
239 zoomUnder = it->zoomLevel();
240 scaleUnder = it->scale();
241 }
242 if ( it->scale() < scale && ( zoomOver == -1 || zoomOver > it->zoomLevel() ) )
243 {
244 zoomOver = it->zoomLevel();
245 scaleOver = it->scale();
246 }
247 }
248
249 if ( zoomUnder < 0 )
250 return zoomOver;
251 if ( zoomOver < 0 )
252 return zoomUnder;
253 else
254 return ( scaleUnder - scale ) / ( scaleUnder - scaleOver ) * ( zoomOver - zoomUnder ) + zoomUnder;
255}
256
257int QgsTileMatrixSet::scaleToZoomLevel( double scale ) const
258{
259 int tileZoom = 0;
260 switch ( mScaleToTileZoomMethod )
261 {
263 tileZoom = static_cast<int>( round( scaleToZoom( scale ) ) );
264 break;
266 tileZoom = static_cast<int>( floor( scaleToZoom( scale ) ) );
267 break;
268 }
269
270 return std::clamp( tileZoom, minimumZoom(), maximumZoom() );
271}
272
274{
275 return calculateTileScaleForMap( context.rendererScale(),
277 context.mapExtent(),
278 context.outputSize(),
279 context.painter()->device()->logicalDpiX() );
280}
281
282double QgsTileMatrixSet::calculateTileScaleForMap( double actualMapScale, const QgsCoordinateReferenceSystem &mapCrs, const QgsRectangle &mapExtent, const QSize mapSize, const double mapDpi ) const
283{
284 switch ( mScaleToTileZoomMethod )
285 {
287 return actualMapScale;
288
290 if ( mapCrs.isGeographic() )
291 {
292 // ESRI calculates the scale for geographic CRS ***ALWAYS*** at the equator, regardless of map extent!
293 // see https://support.esri.com/en/technical-article/000007211, https://gis.stackexchange.com/questions/33270/how-does-arcmap-calculate-scalebar-inside-a-wgs84-layout
294 constexpr double METERS_PER_DEGREE = M_PI / 180.0 * 6378137;
295 constexpr double INCHES_PER_METER = 39.370078;
296 const double mapWidthInches = mapExtent.width() * METERS_PER_DEGREE * INCHES_PER_METER;
297
298 double scale = mapWidthInches * mapDpi / static_cast< double >( mapSize.width() );
299
300 // Note: I **think** there's also some magic which ESRI applies when rendering tiles ON SCREEN,
301 // which may be something like adjusting the scale based on the ratio between the map DPI and 96 DPI,
302 // e.g. scale *= mapDpi / 96.0;
303 // BUT the same adjustment isn't applied when exporting maps. This needs further investigation!
304
305 return scale;
306 }
307 else
308 {
309 return actualMapScale;
310 }
311 }
313}
314
315bool QgsTileMatrixSet::readXml( const QDomElement &element, QgsReadWriteContext & )
316{
317 mTileMatrices.clear();
318
319 mScaleToTileZoomMethod = qgsEnumKeyToValue( element.attribute( QStringLiteral( "scaleToZoomMethod" ) ), Qgis::ScaleToTileZoomLevelMethod::MapBox );
320
321 auto readMatrixFromElement = []( const QDomElement & matrixElement )->QgsTileMatrix
322 {
323 QgsTileMatrix matrix;
324 matrix.mZoomLevel = matrixElement.attribute( QStringLiteral( "zoomLevel" ) ).toInt();
325 matrix.mMatrixWidth = matrixElement.attribute( QStringLiteral( "matrixWidth" ) ).toInt();
326 matrix.mMatrixHeight = matrixElement.attribute( QStringLiteral( "matrixHeight" ) ).toInt();
327 matrix.mExtent = QgsRectangle(
328 matrixElement.attribute( QStringLiteral( "xMin" ) ).toDouble(),
329 matrixElement.attribute( QStringLiteral( "yMin" ) ).toDouble(),
330 matrixElement.attribute( QStringLiteral( "xMax" ) ).toDouble(),
331 matrixElement.attribute( QStringLiteral( "yMax" ) ).toDouble()
332 );
333
334 matrix.mScaleDenom = matrixElement.attribute( QStringLiteral( "scale" ) ).toDouble();
335 matrix.mTileXSpan = matrixElement.attribute( QStringLiteral( "tileXSpan" ) ).toDouble();
336 matrix.mTileYSpan = matrixElement.attribute( QStringLiteral( "tileYSpan" ) ).toDouble();
337 matrix.mCrs.readXml( matrixElement );
338 return matrix;
339 };
340
341 const QDomNodeList children = element.childNodes();
342 for ( int i = 0; i < children.size(); i++ )
343 {
344 const QDomElement matrixElement = children.at( i ).toElement();
345 if ( matrixElement.tagName() == QLatin1String( "rootMatrix" ) )
346 continue;
347
348 QgsTileMatrix matrix = readMatrixFromElement( matrixElement );
349 if ( matrix.zoomLevel() == 0 ) // old project compatibility
350 mRootMatrix = matrix;
351
352 addMatrix( matrix );
353 }
354
355 const QDomElement rootElement = element.firstChildElement( QStringLiteral( "rootMatrix" ) );
356 if ( !rootElement.isNull() )
357 {
358 mRootMatrix = readMatrixFromElement( rootElement );
359 }
360
361 return true;
362}
363
364QDomElement QgsTileMatrixSet::writeXml( QDomDocument &document, const QgsReadWriteContext & ) const
365{
366 QDomElement setElement = document.createElement( QStringLiteral( "matrixSet" ) );
367 setElement.setAttribute( QStringLiteral( "scaleToZoomMethod" ), qgsEnumValueToKey( mScaleToTileZoomMethod ) );
368
369 auto writeMatrixToElement = [&document]( const QgsTileMatrix & matrix, QDomElement & matrixElement )
370 {
371 matrixElement.setAttribute( QStringLiteral( "zoomLevel" ), matrix.zoomLevel() );
372 matrixElement.setAttribute( QStringLiteral( "matrixWidth" ), matrix.matrixWidth() );
373 matrixElement.setAttribute( QStringLiteral( "matrixHeight" ), matrix.matrixHeight() );
374
375 matrixElement.setAttribute( QStringLiteral( "xMin" ), qgsDoubleToString( matrix.mExtent.xMinimum() ) );
376 matrixElement.setAttribute( QStringLiteral( "xMax" ), qgsDoubleToString( matrix.mExtent.xMaximum() ) );
377 matrixElement.setAttribute( QStringLiteral( "yMin" ), qgsDoubleToString( matrix.mExtent.yMinimum() ) );
378 matrixElement.setAttribute( QStringLiteral( "yMax" ), qgsDoubleToString( matrix.mExtent.yMaximum() ) );
379
380 matrixElement.setAttribute( QStringLiteral( "scale" ), qgsDoubleToString( matrix.scale() ) );
381 matrixElement.setAttribute( QStringLiteral( "tileXSpan" ), qgsDoubleToString( matrix.mTileXSpan ) );
382 matrixElement.setAttribute( QStringLiteral( "tileYSpan" ), qgsDoubleToString( matrix.mTileYSpan ) );
383
384 matrix.crs().writeXml( matrixElement, document );
385 };
386
387 for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
388 {
389 QDomElement matrixElement = document.createElement( QStringLiteral( "matrix" ) );
390 writeMatrixToElement( *it, matrixElement );
391 setElement.appendChild( matrixElement );
392 }
393
394 QDomElement rootElement = document.createElement( QStringLiteral( "rootMatrix" ) );
395 writeMatrixToElement( mRootMatrix, rootElement );
396 setElement.appendChild( rootElement );
397
398 return setElement;
399}
@ Esri
No scale doubling, always rounds down when matching to available tile levels.
@ MapBox
Uses a scale doubling approach to account for hi-DPI tiles, and rounds to the nearest tile level for ...
This class represents a coordinate reference system (CRS).
Q_GADGET Qgis::DistanceUnit mapUnits
bool readXml(const QDomNode &node)
Restores state from the given DOM node.
bool writeXml(QDomNode &node, QDomDocument &doc) const
Stores state to the given Dom node in the given document.
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
A class to represent a 2D point.
Definition: qgspointxy.h:59
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
The class is used as a container of context for various read/write operations on other objects.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:183
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
double height() const SIP_HOLDGIL
Returns the height of the rectangle.
Definition: qgsrectangle.h:230
double width() const SIP_HOLDGIL
Returns the width of the rectangle.
Definition: qgsrectangle.h:223
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
double rendererScale() const
Returns the renderer map scale.
QSize outputSize() const
Returns the size of the resulting rendered image, in pixels.
QgsRectangle mapExtent() const
Returns the original extent of the map being rendered.
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
virtual QDomElement writeXml(QDomDocument &document, const QgsReadWriteContext &context) const
Writes the set to an XML element.
Definition: qgstiles.cpp:364
void addGoogleCrs84QuadTiles(int minimumZoom=0, int maximumZoom=14)
Adds tile matrices corresponding to the standard web mercator/GoogleCRS84Quad setup.
Definition: qgstiles.cpp:137
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system associated with the tiles.
Definition: qgstiles.cpp:207
double scaleForRenderContext(const QgsRenderContext &context) const
Calculates the correct scale to use for the tiles when rendered using the specified render context.
Definition: qgstiles.cpp:273
int minimumZoom() const
Returns the minimum zoom level for tiles present in the set.
Definition: qgstiles.cpp:170
int scaleToZoomLevel(double scale) const
Finds the best fitting (integer) zoom level given a map scale denominator.
Definition: qgstiles.cpp:257
double scaleToZoom(double scale) const
Calculates a fractional zoom level given a map scale denominator.
Definition: qgstiles.cpp:215
int maximumZoom() const
Returns the maximum zoom level for tiles present in the set.
Definition: qgstiles.cpp:181
QgsTileMatrix tileMatrix(int zoom) const
Returns the tile matrix corresponding to the specified zoom.
Definition: qgstiles.cpp:150
QgsTileMatrix rootMatrix() const
Returns the root tile matrix (usually corresponding to zoom level 0).
Definition: qgstiles.cpp:155
virtual bool readXml(const QDomElement &element, QgsReadWriteContext &context)
Reads the set from an XML element.
Definition: qgstiles.cpp:315
void addMatrix(const QgsTileMatrix &matrix)
Adds a matrix to the set.
Definition: qgstiles.cpp:165
double calculateTileScaleForMap(double actualMapScale, const QgsCoordinateReferenceSystem &mapCrs, const QgsRectangle &mapExtent, const QSize mapSize, const double mapDpi) const
Calculates the correct scale to use for the tiles when rendered using the specified map properties.
Definition: qgstiles.cpp:282
void dropMatricesOutsideZoomRange(int minimumZoom, int maximumZoom)
Deletes any existing matrices which fall outside the zoom range specified by minimumZoom to maximumZo...
Definition: qgstiles.cpp:192
bool isEmpty() const
Returns true if the matrix set is empty.
Definition: qgstiles.cpp:132
void setRootMatrix(const QgsTileMatrix &matrix)
Sets the root tile matrix (usually corresponding to zoom level 0).
Definition: qgstiles.cpp:160
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition: qgstiles.h:108
QgsRectangle tileExtent(QgsTileXYZ id) const
Returns extent of the given tile in this matrix.
Definition: qgstiles.cpp:81
QPointF mapToTileCoordinates(const QgsPointXY &mapPoint) const
Returns row/column coordinates (floating point number) from the given point in map coordinates.
Definition: qgstiles.cpp:121
QgsTileRange tileRangeFromExtent(const QgsRectangle &mExtent) const
Returns tile range that fully covers the given extent.
Definition: qgstiles.cpp:97
static QgsTileMatrix fromWebMercator(int zoomLevel)
Returns a tile matrix for the usual web mercator.
Definition: qgstiles.cpp:23
QgsRectangle extent() const
Returns extent of the tile matrix.
Definition: qgstiles.h:163
int matrixWidth() const
Returns number of columns of the tile matrix.
Definition: qgstiles.h:157
QgsCoordinateReferenceSystem crs() const
Returns the crs of the tile matrix.
Definition: qgstiles.h:131
double scale() const
Returns scale denominator of the tile matrix.
Definition: qgstiles.h:170
QgsPointXY tileCenter(QgsTileXYZ id) const
Returns center of the given tile in this matrix.
Definition: qgstiles.cpp:90
int matrixHeight() const
Returns number of rows of the tile matrix.
Definition: qgstiles.h:160
static QgsTileMatrix fromTileMatrix(int zoomLevel, const QgsTileMatrix &tileMatrix)
Returns a tile matrix based on another one.
Definition: qgstiles.cpp:61
int zoomLevel() const
Returns the zoom level of the tile matrix.
Definition: qgstiles.h:146
static QgsTileMatrix fromCustomDef(int zoomLevel, const QgsCoordinateReferenceSystem &crs, const QgsPointXY &z0TopLeftPoint, double z0Dimension, int z0MatrixWidth=1, int z0MatrixHeight=1)
Returns a tile matrix for a specific CRS, top left point, zoom level 0 dimension in CRS units.
Definition: qgstiles.cpp:31
Range of tiles in a tile matrix to be rendered.
Definition: qgstiles.h:71
Stores coordinates of a tile in a tile matrix set.
Definition: qgstiles.h:38
static Q_INVOKABLE double fromUnitToUnitFactor(Qgis::DistanceUnit fromUnit, Qgis::DistanceUnit toUnit)
Returns the conversion factor between the specified distance units.
T qgsEnumKeyToValue(const QString &key, const T &defaultValue, bool tryValueAsKey=true, bool *returnOk=nullptr)
Returns the value corresponding to the given key of an enum.
Definition: qgis.h:3732
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:3448
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition: qgis.h:3713
#define BUILTIN_UNREACHABLE
Definition: qgis.h:4180
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
const QgsCoordinateReferenceSystem & crs