MapProxy 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (459) hide show
  1. MapProxy-2.1.0.dist-info/AUTHORS.txt +33 -0
  2. MapProxy-2.1.0.dist-info/COPYING.txt +60 -0
  3. MapProxy-2.1.0.dist-info/LICENSE.txt +202 -0
  4. MapProxy-2.1.0.dist-info/METADATA +165 -0
  5. MapProxy-2.1.0.dist-info/RECORD +459 -0
  6. MapProxy-2.1.0.dist-info/WHEEL +5 -0
  7. MapProxy-2.1.0.dist-info/entry_points.txt +4 -0
  8. MapProxy-2.1.0.dist-info/top_level.txt +1 -0
  9. mapproxy/__init__.py +0 -0
  10. mapproxy/cache/__init__.py +36 -0
  11. mapproxy/cache/azureblob.py +145 -0
  12. mapproxy/cache/base.py +120 -0
  13. mapproxy/cache/compact.py +665 -0
  14. mapproxy/cache/couchdb.py +301 -0
  15. mapproxy/cache/dummy.py +36 -0
  16. mapproxy/cache/file.py +200 -0
  17. mapproxy/cache/geopackage.py +647 -0
  18. mapproxy/cache/legend.py +87 -0
  19. mapproxy/cache/mbtiles.py +411 -0
  20. mapproxy/cache/meta.py +80 -0
  21. mapproxy/cache/path.py +261 -0
  22. mapproxy/cache/redis.py +152 -0
  23. mapproxy/cache/renderd.py +100 -0
  24. mapproxy/cache/riak.py +206 -0
  25. mapproxy/cache/s3.py +209 -0
  26. mapproxy/cache/tile.py +736 -0
  27. mapproxy/client/__init__.py +0 -0
  28. mapproxy/client/arcgis.py +82 -0
  29. mapproxy/client/cgi.py +141 -0
  30. mapproxy/client/http.py +295 -0
  31. mapproxy/client/log.py +33 -0
  32. mapproxy/client/tile.py +158 -0
  33. mapproxy/client/wms.py +255 -0
  34. mapproxy/compat/__init__.py +0 -0
  35. mapproxy/compat/image.py +86 -0
  36. mapproxy/config/__init__.py +22 -0
  37. mapproxy/config/config-schema.json +813 -0
  38. mapproxy/config/config.py +213 -0
  39. mapproxy/config/coverage.py +108 -0
  40. mapproxy/config/defaults.py +102 -0
  41. mapproxy/config/loader.py +2399 -0
  42. mapproxy/config/spec.py +657 -0
  43. mapproxy/config/validator.py +242 -0
  44. mapproxy/config_template/__init__.py +0 -0
  45. mapproxy/config_template/base_config/config.wsgi +10 -0
  46. mapproxy/config_template/base_config/full_example.yaml +598 -0
  47. mapproxy/config_template/base_config/full_seed_example.yaml +79 -0
  48. mapproxy/config_template/base_config/log.ini +35 -0
  49. mapproxy/config_template/base_config/mapproxy.yaml +60 -0
  50. mapproxy/config_template/base_config/seed.yaml +27 -0
  51. mapproxy/exception.py +149 -0
  52. mapproxy/featureinfo.py +251 -0
  53. mapproxy/grid.py +1199 -0
  54. mapproxy/image/__init__.py +549 -0
  55. mapproxy/image/fonts/DejaVuSans.ttf +0 -0
  56. mapproxy/image/fonts/DejaVuSansMono.ttf +0 -0
  57. mapproxy/image/fonts/LICENSE +99 -0
  58. mapproxy/image/fonts/__init__.py +0 -0
  59. mapproxy/image/mask.py +79 -0
  60. mapproxy/image/merge.py +323 -0
  61. mapproxy/image/message.py +357 -0
  62. mapproxy/image/opts.py +185 -0
  63. mapproxy/image/tile.py +171 -0
  64. mapproxy/image/transform.py +350 -0
  65. mapproxy/layer.py +489 -0
  66. mapproxy/multiapp.py +230 -0
  67. mapproxy/proj.py +309 -0
  68. mapproxy/request/__init__.py +18 -0
  69. mapproxy/request/arcgis.py +268 -0
  70. mapproxy/request/base.py +466 -0
  71. mapproxy/request/tile.py +131 -0
  72. mapproxy/request/wms/__init__.py +829 -0
  73. mapproxy/request/wms/exception.py +107 -0
  74. mapproxy/request/wmts.py +442 -0
  75. mapproxy/response.py +237 -0
  76. mapproxy/script/__init__.py +0 -0
  77. mapproxy/script/conf/__init__.py +0 -0
  78. mapproxy/script/conf/app.py +222 -0
  79. mapproxy/script/conf/caches.py +44 -0
  80. mapproxy/script/conf/geopackage.py +136 -0
  81. mapproxy/script/conf/layers.py +54 -0
  82. mapproxy/script/conf/seeds.py +36 -0
  83. mapproxy/script/conf/sources.py +88 -0
  84. mapproxy/script/conf/utils.py +148 -0
  85. mapproxy/script/defrag.py +187 -0
  86. mapproxy/script/export.py +337 -0
  87. mapproxy/script/grids.py +198 -0
  88. mapproxy/script/scales.py +134 -0
  89. mapproxy/script/util.py +410 -0
  90. mapproxy/script/wms_capabilities.py +160 -0
  91. mapproxy/seed/__init__.py +0 -0
  92. mapproxy/seed/cachelock.py +127 -0
  93. mapproxy/seed/cleanup.py +191 -0
  94. mapproxy/seed/config.py +481 -0
  95. mapproxy/seed/script.py +391 -0
  96. mapproxy/seed/seeder.py +551 -0
  97. mapproxy/seed/spec.py +66 -0
  98. mapproxy/seed/util.py +266 -0
  99. mapproxy/service/__init__.py +14 -0
  100. mapproxy/service/base.py +45 -0
  101. mapproxy/service/demo.py +364 -0
  102. mapproxy/service/kml.py +333 -0
  103. mapproxy/service/ows.py +39 -0
  104. mapproxy/service/template_helper.py +55 -0
  105. mapproxy/service/templates/demo/capabilities_demo.html +18 -0
  106. mapproxy/service/templates/demo/demo.html +181 -0
  107. mapproxy/service/templates/demo/openlayers-demo.cfg +16 -0
  108. mapproxy/service/templates/demo/static/img/blank.gif +0 -0
  109. mapproxy/service/templates/demo/static/img/east-mini.png +0 -0
  110. mapproxy/service/templates/demo/static/img/north-mini.png +0 -0
  111. mapproxy/service/templates/demo/static/img/south-mini.png +0 -0
  112. mapproxy/service/templates/demo/static/img/west-mini.png +0 -0
  113. mapproxy/service/templates/demo/static/img/zoom-minus-mini.png +0 -0
  114. mapproxy/service/templates/demo/static/img/zoom-plus-mini.png +0 -0
  115. mapproxy/service/templates/demo/static/img/zoom-world-mini.png +0 -0
  116. mapproxy/service/templates/demo/static/logo.png +0 -0
  117. mapproxy/service/templates/demo/static/ol.css +345 -0
  118. mapproxy/service/templates/demo/static/ol.js +4 -0
  119. mapproxy/service/templates/demo/static/proj4.min.js +1 -0
  120. mapproxy/service/templates/demo/static/proj4defs.js +1 -0
  121. mapproxy/service/templates/demo/static/site.css +137 -0
  122. mapproxy/service/templates/demo/static/theme/default/framedCloud.css +0 -0
  123. mapproxy/service/templates/demo/static/theme/default/google.css +17 -0
  124. mapproxy/service/templates/demo/static/theme/default/ie6-style.css +10 -0
  125. mapproxy/service/templates/demo/static/theme/default/style.css +482 -0
  126. mapproxy/service/templates/demo/static.html +34 -0
  127. mapproxy/service/templates/demo/tms_demo.html +117 -0
  128. mapproxy/service/templates/demo/wms_demo.html +144 -0
  129. mapproxy/service/templates/demo/wmts_demo.html +118 -0
  130. mapproxy/service/templates/tms_capabilities.xml +13 -0
  131. mapproxy/service/templates/tms_exception.xml +4 -0
  132. mapproxy/service/templates/tms_root_resource.xml +7 -0
  133. mapproxy/service/templates/tms_tilemap_capabilities.xml +14 -0
  134. mapproxy/service/templates/wms100capabilities.xml +112 -0
  135. mapproxy/service/templates/wms100exception.xml +4 -0
  136. mapproxy/service/templates/wms110capabilities.xml +152 -0
  137. mapproxy/service/templates/wms110exception.xml +5 -0
  138. mapproxy/service/templates/wms111capabilities.xml +183 -0
  139. mapproxy/service/templates/wms111exception.xml +5 -0
  140. mapproxy/service/templates/wms130capabilities.xml +326 -0
  141. mapproxy/service/templates/wms130exception.xml +8 -0
  142. mapproxy/service/templates/wmts100capabilities.xml +155 -0
  143. mapproxy/service/templates/wmts100exception.xml +9 -0
  144. mapproxy/service/tile.py +540 -0
  145. mapproxy/service/wms.py +868 -0
  146. mapproxy/service/wmts.py +387 -0
  147. mapproxy/source/__init__.py +83 -0
  148. mapproxy/source/arcgis.py +39 -0
  149. mapproxy/source/error.py +40 -0
  150. mapproxy/source/mapnik.py +262 -0
  151. mapproxy/source/tile.py +97 -0
  152. mapproxy/source/wms.py +273 -0
  153. mapproxy/srs.py +734 -0
  154. mapproxy/template.py +54 -0
  155. mapproxy/test/__init__.py +0 -0
  156. mapproxy/test/conftest.py +8 -0
  157. mapproxy/test/helper.py +255 -0
  158. mapproxy/test/http.py +511 -0
  159. mapproxy/test/image.py +219 -0
  160. mapproxy/test/mocker.py +2291 -0
  161. mapproxy/test/schemas/inspire/common/1.0/common.xsd +1461 -0
  162. mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd +108 -0
  163. mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd +108 -0
  164. mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd +108 -0
  165. mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd +108 -0
  166. mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd +155 -0
  167. mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd +108 -0
  168. mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd +108 -0
  169. mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd +108 -0
  170. mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd +108 -0
  171. mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd +109 -0
  172. mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd +108 -0
  173. mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd +108 -0
  174. mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd +108 -0
  175. mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd +108 -0
  176. mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd +108 -0
  177. mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd +108 -0
  178. mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd +108 -0
  179. mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd +108 -0
  180. mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd +108 -0
  181. mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd +108 -0
  182. mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd +108 -0
  183. mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd +108 -0
  184. mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd +108 -0
  185. mapproxy/test/schemas/inspire/common/1.0/network.xsd +521 -0
  186. mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd +19 -0
  187. mapproxy/test/schemas/kml/2.2.0/ReadMe.txt +14 -0
  188. mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd +66 -0
  189. mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd +1646 -0
  190. mapproxy/test/schemas/kml/2.2.0/xAL.xsd +1680 -0
  191. mapproxy/test/schemas/ows/1.1.0/ReadMe.txt +87 -0
  192. mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd +235 -0
  193. mapproxy/test/schemas/ows/1.1.0/owsAll.xsd +23 -0
  194. mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd +157 -0
  195. mapproxy/test/schemas/ows/1.1.0/owsContents.xsd +86 -0
  196. mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd +127 -0
  197. mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd +279 -0
  198. mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd +76 -0
  199. mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd +112 -0
  200. mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd +51 -0
  201. mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd +59 -0
  202. mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd +125 -0
  203. mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd +140 -0
  204. mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd +60 -0
  205. mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd +47 -0
  206. mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd +27 -0
  207. mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd +353 -0
  208. mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml +188 -0
  209. mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd +524 -0
  210. mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml +260 -0
  211. mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd +273 -0
  212. mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml +303 -0
  213. mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd +6 -0
  214. mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml +33 -0
  215. mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd +68 -0
  216. mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd +22 -0
  217. mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd +274 -0
  218. mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd +5 -0
  219. mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd +276 -0
  220. mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml +303 -0
  221. mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd +6 -0
  222. mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml +33 -0
  223. mapproxy/test/schemas/wms/1.3.0/ReadMe.txt +8 -0
  224. mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml +277 -0
  225. mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd +611 -0
  226. mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml +34 -0
  227. mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd +28 -0
  228. mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd +68 -0
  229. mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd +22 -0
  230. mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd +283 -0
  231. mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd +5 -0
  232. mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd +276 -0
  233. mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml +303 -0
  234. mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd +6 -0
  235. mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml +33 -0
  236. mapproxy/test/schemas/wmts/1.0/ReadMe.txt +32 -0
  237. mapproxy/test/schemas/wmts/1.0/wmts.xsd +28 -0
  238. mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl +151 -0
  239. mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd +38 -0
  240. mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd +564 -0
  241. mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd +57 -0
  242. mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd +72 -0
  243. mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd +91 -0
  244. mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd +76 -0
  245. mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd +70 -0
  246. mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt +6 -0
  247. mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd +122 -0
  248. mapproxy/test/schemas/xml.xsd +287 -0
  249. mapproxy/test/system/__init__.py +98 -0
  250. mapproxy/test/system/fixture/arcgis.yaml +57 -0
  251. mapproxy/test/system/fixture/auth.yaml +70 -0
  252. mapproxy/test/system/fixture/cache.mbtiles +0 -0
  253. mapproxy/test/system/fixture/cache_azureblob.yaml +59 -0
  254. mapproxy/test/system/fixture/cache_band_merge.yaml +73 -0
  255. mapproxy/test/system/fixture/cache_bulk_meta_tiles.yaml +24 -0
  256. mapproxy/test/system/fixture/cache_coverage.yaml +84 -0
  257. mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000/000/000/000.png +0 -0
  258. mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg +0 -0
  259. mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000/001.png +0 -0
  260. mapproxy/test/system/fixture/cache_geopackage.yaml +56 -0
  261. mapproxy/test/system/fixture/cache_grid_names.yaml +50 -0
  262. mapproxy/test/system/fixture/cache_mbtiles.yaml +28 -0
  263. mapproxy/test/system/fixture/cache_s3.yaml +58 -0
  264. mapproxy/test/system/fixture/cache_source.yaml +81 -0
  265. mapproxy/test/system/fixture/combined_sources.yaml +130 -0
  266. mapproxy/test/system/fixture/coverage.yaml +77 -0
  267. mapproxy/test/system/fixture/demo.yaml +135 -0
  268. mapproxy/test/system/fixture/dimension.yaml +59 -0
  269. mapproxy/test/system/fixture/disable_storage.yaml +25 -0
  270. mapproxy/test/system/fixture/empty_ogrdata.geojson +1 -0
  271. mapproxy/test/system/fixture/formats.yaml +72 -0
  272. mapproxy/test/system/fixture/inspire.yaml +101 -0
  273. mapproxy/test/system/fixture/inspire_full.yaml +124 -0
  274. mapproxy/test/system/fixture/kml_layer.yaml +66 -0
  275. mapproxy/test/system/fixture/layer.yaml +260 -0
  276. mapproxy/test/system/fixture/layergroups.yaml +57 -0
  277. mapproxy/test/system/fixture/layergroups_root.yaml +106 -0
  278. mapproxy/test/system/fixture/legendgraphic.yaml +93 -0
  279. mapproxy/test/system/fixture/mapnik_source.yaml +66 -0
  280. mapproxy/test/system/fixture/mapproxy_export.yaml +12 -0
  281. mapproxy/test/system/fixture/mapserver.yaml +23 -0
  282. mapproxy/test/system/fixture/minimal_cgi.py +16 -0
  283. mapproxy/test/system/fixture/mixed_mode.yaml +49 -0
  284. mapproxy/test/system/fixture/multi_cache_layers.yaml +111 -0
  285. mapproxy/test/system/fixture/multiapp1.yaml +20 -0
  286. mapproxy/test/system/fixture/multiapp2.yaml +19 -0
  287. mapproxy/test/system/fixture/renderd_client.yaml +55 -0
  288. mapproxy/test/system/fixture/scalehints.yaml +70 -0
  289. mapproxy/test/system/fixture/seed.yaml +94 -0
  290. mapproxy/test/system/fixture/seed_mapproxy.yaml +39 -0
  291. mapproxy/test/system/fixture/seed_old.yaml +12 -0
  292. mapproxy/test/system/fixture/seed_timeouts.yaml +12 -0
  293. mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml +27 -0
  294. mapproxy/test/system/fixture/seedonly.yaml +51 -0
  295. mapproxy/test/system/fixture/sld.yaml +35 -0
  296. mapproxy/test/system/fixture/source_errors.yaml +84 -0
  297. mapproxy/test/system/fixture/source_errors_raise.yaml +82 -0
  298. mapproxy/test/system/fixture/tileservice_origin.yaml +26 -0
  299. mapproxy/test/system/fixture/tileservice_refresh.yaml +59 -0
  300. mapproxy/test/system/fixture/tilesource_minmax_res.yaml +22 -0
  301. mapproxy/test/system/fixture/util-conf-base-grids.yaml +5 -0
  302. mapproxy/test/system/fixture/util-conf-overwrite.yaml +13 -0
  303. mapproxy/test/system/fixture/util-conf-wms-111-cap.xml +90 -0
  304. mapproxy/test/system/fixture/util_grids.yaml +29 -0
  305. mapproxy/test/system/fixture/util_wms_capabilities111.xml +130 -0
  306. mapproxy/test/system/fixture/util_wms_capabilities130.xml +100 -0
  307. mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml +5 -0
  308. mapproxy/test/system/fixture/watermark.yaml +50 -0
  309. mapproxy/test/system/fixture/wms_srs_extent.yaml +39 -0
  310. mapproxy/test/system/fixture/wms_versions.yaml +38 -0
  311. mapproxy/test/system/fixture/wmts.yaml +134 -0
  312. mapproxy/test/system/fixture/wmts_dimensions.yaml +57 -0
  313. mapproxy/test/system/fixture/xslt_featureinfo.yaml +54 -0
  314. mapproxy/test/system/fixture/xslt_featureinfo_input.yaml +51 -0
  315. mapproxy/test/system/test_arcgis.py +156 -0
  316. mapproxy/test/system/test_auth.py +1133 -0
  317. mapproxy/test/system/test_behind_proxy.py +75 -0
  318. mapproxy/test/system/test_bulk_meta_tiles.py +106 -0
  319. mapproxy/test/system/test_cache_azureblob.py +127 -0
  320. mapproxy/test/system/test_cache_band_merge.py +103 -0
  321. mapproxy/test/system/test_cache_coverage.py +168 -0
  322. mapproxy/test/system/test_cache_geopackage.py +144 -0
  323. mapproxy/test/system/test_cache_grid_names.py +89 -0
  324. mapproxy/test/system/test_cache_mbtiles.py +85 -0
  325. mapproxy/test/system/test_cache_s3.py +115 -0
  326. mapproxy/test/system/test_cache_source.py +146 -0
  327. mapproxy/test/system/test_combined_sources.py +335 -0
  328. mapproxy/test/system/test_coverage.py +140 -0
  329. mapproxy/test/system/test_decorate_img.py +214 -0
  330. mapproxy/test/system/test_demo.py +56 -0
  331. mapproxy/test/system/test_demo_with_extra_service.py +57 -0
  332. mapproxy/test/system/test_dimensions.py +279 -0
  333. mapproxy/test/system/test_disable_storage.py +42 -0
  334. mapproxy/test/system/test_formats.py +219 -0
  335. mapproxy/test/system/test_inspire_vs.py +173 -0
  336. mapproxy/test/system/test_kml.py +264 -0
  337. mapproxy/test/system/test_layergroups.py +160 -0
  338. mapproxy/test/system/test_legendgraphic.py +308 -0
  339. mapproxy/test/system/test_mapnik.py +162 -0
  340. mapproxy/test/system/test_mapserver.py +83 -0
  341. mapproxy/test/system/test_mixed_mode_format.py +201 -0
  342. mapproxy/test/system/test_multi_cache_layers.py +169 -0
  343. mapproxy/test/system/test_multiapp.py +92 -0
  344. mapproxy/test/system/test_refresh.py +206 -0
  345. mapproxy/test/system/test_renderd_client.py +304 -0
  346. mapproxy/test/system/test_response_headers.py +54 -0
  347. mapproxy/test/system/test_scalehints.py +140 -0
  348. mapproxy/test/system/test_seed.py +425 -0
  349. mapproxy/test/system/test_seed_only.py +93 -0
  350. mapproxy/test/system/test_sld.py +120 -0
  351. mapproxy/test/system/test_source_errors.py +377 -0
  352. mapproxy/test/system/test_tilesource_minmax_res.py +54 -0
  353. mapproxy/test/system/test_tms.py +277 -0
  354. mapproxy/test/system/test_tms_origin.py +46 -0
  355. mapproxy/test/system/test_util_conf.py +434 -0
  356. mapproxy/test/system/test_util_export.py +210 -0
  357. mapproxy/test/system/test_util_grids.py +88 -0
  358. mapproxy/test/system/test_util_wms_capabilities.py +182 -0
  359. mapproxy/test/system/test_watermark.py +91 -0
  360. mapproxy/test/system/test_wms.py +1616 -0
  361. mapproxy/test/system/test_wms_srs_extent.py +165 -0
  362. mapproxy/test/system/test_wms_version.py +85 -0
  363. mapproxy/test/system/test_wmsc.py +116 -0
  364. mapproxy/test/system/test_wmts.py +334 -0
  365. mapproxy/test/system/test_wmts_dimensions.py +206 -0
  366. mapproxy/test/system/test_wmts_restful.py +198 -0
  367. mapproxy/test/system/test_xslt_featureinfo.py +423 -0
  368. mapproxy/test/test_http_helper.py +217 -0
  369. mapproxy/test/unit/__init__.py +0 -0
  370. mapproxy/test/unit/epsg +2 -0
  371. mapproxy/test/unit/polygons/polygons.dbf +0 -0
  372. mapproxy/test/unit/polygons/polygons.shp +0 -0
  373. mapproxy/test/unit/polygons/polygons.shx +0 -0
  374. mapproxy/test/unit/test_async.py +242 -0
  375. mapproxy/test/unit/test_auth.py +430 -0
  376. mapproxy/test/unit/test_cache.py +1356 -0
  377. mapproxy/test/unit/test_cache_azureblob.py +97 -0
  378. mapproxy/test/unit/test_cache_compact.py +324 -0
  379. mapproxy/test/unit/test_cache_couchdb.py +118 -0
  380. mapproxy/test/unit/test_cache_geopackage.py +256 -0
  381. mapproxy/test/unit/test_cache_redis.py +123 -0
  382. mapproxy/test/unit/test_cache_riak.py +80 -0
  383. mapproxy/test/unit/test_cache_s3.py +93 -0
  384. mapproxy/test/unit/test_cache_tile.py +477 -0
  385. mapproxy/test/unit/test_client.py +488 -0
  386. mapproxy/test/unit/test_client_arcgis.py +74 -0
  387. mapproxy/test/unit/test_client_cgi.py +140 -0
  388. mapproxy/test/unit/test_collections.py +116 -0
  389. mapproxy/test/unit/test_concat_legends.py +37 -0
  390. mapproxy/test/unit/test_conf_loader.py +1267 -0
  391. mapproxy/test/unit/test_conf_validator.py +427 -0
  392. mapproxy/test/unit/test_config.py +118 -0
  393. mapproxy/test/unit/test_decorate_img.py +185 -0
  394. mapproxy/test/unit/test_exceptions.py +270 -0
  395. mapproxy/test/unit/test_featureinfo.py +313 -0
  396. mapproxy/test/unit/test_file_lock_load.py +49 -0
  397. mapproxy/test/unit/test_geom.py +512 -0
  398. mapproxy/test/unit/test_grid.py +1279 -0
  399. mapproxy/test/unit/test_image.py +1051 -0
  400. mapproxy/test/unit/test_image_mask.py +181 -0
  401. mapproxy/test/unit/test_image_messages.py +209 -0
  402. mapproxy/test/unit/test_image_options.py +160 -0
  403. mapproxy/test/unit/test_isodate.py +118 -0
  404. mapproxy/test/unit/test_multiapp.py +163 -0
  405. mapproxy/test/unit/test_ogr_reader.py +51 -0
  406. mapproxy/test/unit/test_request.py +745 -0
  407. mapproxy/test/unit/test_request_wmts.py +178 -0
  408. mapproxy/test/unit/test_response.py +78 -0
  409. mapproxy/test/unit/test_seed.py +365 -0
  410. mapproxy/test/unit/test_seed_cachelock.py +91 -0
  411. mapproxy/test/unit/test_srs.py +215 -0
  412. mapproxy/test/unit/test_tiled_source.py +122 -0
  413. mapproxy/test/unit/test_tilefilter.py +31 -0
  414. mapproxy/test/unit/test_times.py +25 -0
  415. mapproxy/test/unit/test_timeutils.py +50 -0
  416. mapproxy/test/unit/test_util_conf_utils.py +75 -0
  417. mapproxy/test/unit/test_utils.py +476 -0
  418. mapproxy/test/unit/test_wms_capabilities.py +44 -0
  419. mapproxy/test/unit/test_wms_layer.py +113 -0
  420. mapproxy/test/unit/test_yaml.py +68 -0
  421. mapproxy/tilefilter.py +61 -0
  422. mapproxy/util/__init__.py +0 -0
  423. mapproxy/util/async_.py +229 -0
  424. mapproxy/util/collections.py +134 -0
  425. mapproxy/util/coverage.py +337 -0
  426. mapproxy/util/ext/__init__.py +14 -0
  427. mapproxy/util/ext/dictspec/__init__.py +1 -0
  428. mapproxy/util/ext/dictspec/spec.py +131 -0
  429. mapproxy/util/ext/dictspec/test/__init__.py +0 -0
  430. mapproxy/util/ext/dictspec/test/test_validator.py +278 -0
  431. mapproxy/util/ext/dictspec/validator.py +194 -0
  432. mapproxy/util/ext/local.py +198 -0
  433. mapproxy/util/ext/lockfile.py +140 -0
  434. mapproxy/util/ext/odict.py +321 -0
  435. mapproxy/util/ext/serving.py +491 -0
  436. mapproxy/util/ext/tempita/__init__.py +1093 -0
  437. mapproxy/util/ext/tempita/_looper.py +163 -0
  438. mapproxy/util/ext/tempita/string_utils.py +24 -0
  439. mapproxy/util/ext/wmsparse/__init__.py +3 -0
  440. mapproxy/util/ext/wmsparse/duration.py +600 -0
  441. mapproxy/util/ext/wmsparse/parse.py +307 -0
  442. mapproxy/util/ext/wmsparse/test/__init__.py +0 -0
  443. mapproxy/util/ext/wmsparse/test/test_parse.py +111 -0
  444. mapproxy/util/ext/wmsparse/test/test_util.py +23 -0
  445. mapproxy/util/ext/wmsparse/test/wms-example-111.xml +90 -0
  446. mapproxy/util/ext/wmsparse/test/wms-example-130.xml +120 -0
  447. mapproxy/util/ext/wmsparse/test/wms-large-111.xml +2114 -0
  448. mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml +386 -0
  449. mapproxy/util/ext/wmsparse/util.py +189 -0
  450. mapproxy/util/fs.py +164 -0
  451. mapproxy/util/geom.py +307 -0
  452. mapproxy/util/lib.py +117 -0
  453. mapproxy/util/lock.py +171 -0
  454. mapproxy/util/ogr.py +247 -0
  455. mapproxy/util/py.py +75 -0
  456. mapproxy/util/times.py +78 -0
  457. mapproxy/util/yaml.py +58 -0
  458. mapproxy/version.py +33 -0
  459. mapproxy/wsgiapp.py +167 -0
@@ -0,0 +1,2399 @@
1
+ # This file is part of the MapProxy project.
2
+ # Copyright (C) 2010-2016 Omniscale <http://omniscale.de>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """
17
+ Configuration loading and system initializing.
18
+ """
19
+ from __future__ import division
20
+ from mapproxy.util.fs import find_exec
21
+ from mapproxy.util.yaml import load_yaml_file, YAMLError
22
+ from mapproxy.util.ext.odict import odict
23
+ from mapproxy.util.py import memoize
24
+ from mapproxy.config.spec import validate_options, add_source_to_mapproxy_yaml_spec, add_service_to_mapproxy_yaml_spec
25
+ from mapproxy.config.validator import validate
26
+ from mapproxy.config import load_default_config, finish_base_config, defaults
27
+
28
+ import os
29
+ import sys
30
+ import hashlib
31
+ import warnings
32
+ from copy import deepcopy, copy
33
+ from functools import partial
34
+
35
+ import logging
36
+ from urllib.parse import urlparse
37
+
38
+ log = logging.getLogger('mapproxy.config')
39
+
40
+
41
+ class ConfigurationError(Exception):
42
+ pass
43
+
44
+
45
+ class ProxyConfiguration(object):
46
+ def __init__(self, conf, conf_base_dir=None, seed=False, renderd=False):
47
+ self.configuration = conf
48
+ self.seed = seed
49
+ self.renderd = renderd
50
+
51
+ if conf_base_dir is None:
52
+ conf_base_dir = os.getcwd()
53
+
54
+ self.load_globals(conf_base_dir=conf_base_dir)
55
+ self.load_grids()
56
+ self.load_caches()
57
+ self.load_sources()
58
+ self.load_wms_root_layer()
59
+ self.load_tile_layers()
60
+ self.load_services()
61
+
62
+ def load_globals(self, conf_base_dir):
63
+ self.globals = GlobalConfiguration(conf_base_dir=conf_base_dir,
64
+ conf=self.configuration.get('globals') or {},
65
+ context=self)
66
+
67
+ def load_grids(self):
68
+ self.grids = {}
69
+ grid_configs = dict(defaults.grids)
70
+ grid_configs.update(self.configuration.get('grids') or {})
71
+ for grid_name, grid_conf in grid_configs.items():
72
+ grid_conf.setdefault('name', grid_name)
73
+ self.grids[grid_name] = GridConfiguration(grid_conf, context=self)
74
+
75
+ def load_caches(self):
76
+ self.caches = odict()
77
+ caches_conf = self.configuration.get('caches')
78
+ if not caches_conf:
79
+ return
80
+ if isinstance(caches_conf, list):
81
+ caches_conf = list_of_dicts_to_ordered_dict(caches_conf)
82
+ for cache_name, cache_conf in caches_conf.items():
83
+ cache_conf['name'] = cache_name
84
+ self.caches[cache_name] = CacheConfiguration(conf=cache_conf, context=self)
85
+
86
+ def load_sources(self):
87
+ self.sources = SourcesCollection()
88
+ for source_name, source_conf in (self.configuration.get('sources') or {}).items():
89
+ source_conf['name'] = source_name
90
+ self.sources[source_name] = SourceConfiguration.load(conf=source_conf, context=self)
91
+
92
+ def load_tile_layers(self):
93
+ self.layers = odict()
94
+ layers_conf = deepcopy(self._layers_conf_dict())
95
+ if layers_conf is None:
96
+ return
97
+ layers = self._flatten_layers_conf_dict(layers_conf)
98
+ for layer_name, layer_conf in layers.items():
99
+ layer_conf['name'] = layer_name
100
+ self.layers[layer_name] = LayerConfiguration(conf=layer_conf, context=self)
101
+
102
+ def _legacy_layers_conf_dict(self):
103
+ """
104
+ Read old style layer configuration with a dictionary where
105
+ the key is the layer name. Optionally: a list an each layer
106
+ is wrapped in such dictionary.
107
+
108
+ ::
109
+ layers:
110
+ foo:
111
+ title: xxx
112
+ sources: []
113
+ bar:
114
+ title: xxx
115
+ sources: []
116
+
117
+ or
118
+
119
+ ::
120
+
121
+ layers:
122
+ - foo:
123
+ title: xxx
124
+ sources: []
125
+ - bar:
126
+ title: xxx
127
+ sources: []
128
+
129
+ """
130
+ warnings.warn('old layer configuration syntax is deprecated since 1.4.0. '
131
+ 'use list of dictionaries as documented', RuntimeWarning)
132
+ layers = []
133
+ layers_conf = self.configuration.get('layers')
134
+ if not layers_conf:
135
+ return None # TODO config error
136
+ if isinstance(layers_conf, list):
137
+ layers_conf = list_of_dicts_to_ordered_dict(layers_conf)
138
+ for layer_name, layer_conf in layers_conf.items():
139
+ layer_conf['name'] = layer_name
140
+ layers.append(layer_conf)
141
+ return dict(title=None, layers=layers)
142
+
143
+ def _layers_conf_dict(self):
144
+ """
145
+ Returns (recursive) layer configuration as a dictionary
146
+ in unified structure:
147
+
148
+ ::
149
+ {
150
+ title: 'xxx', # required, might be None
151
+ name: 'xxx', # optional
152
+ # sources or layers or both are required
153
+ sources: [],
154
+ layers: [
155
+ {..., ...} # more layers like this
156
+ ]
157
+ }
158
+
159
+ Multiple layers will be wrapped in an unnamed root layer, if the
160
+ first level starts with multiple layers.
161
+ """
162
+ layers_conf = self.configuration.get('layers')
163
+ if layers_conf is None:
164
+ return
165
+
166
+ if isinstance(layers_conf, list):
167
+ if isinstance(layers_conf[0], dict) and len(layers_conf[0].keys()) == 1:
168
+ # looks like ordered legacy config
169
+ layers_conf = self._legacy_layers_conf_dict()
170
+ elif len(layers_conf) == 1 and (
171
+ 'layers' in layers_conf[0]
172
+ or 'sources' in layers_conf[0]
173
+ or 'tile_sources' in layers_conf[0]):
174
+ # single root layer in list -> remove list
175
+ layers_conf = layers_conf[0]
176
+ else:
177
+ # layer list without root -> wrap in root layer
178
+ layers_conf = dict(title=None, layers=layers_conf)
179
+
180
+ if len(set(layers_conf.keys()) &
181
+ set('layers name title sources'.split())) < 2:
182
+ # looks like unordered legacy config
183
+ layers_conf = self._legacy_layers_conf_dict()
184
+
185
+ return layers_conf
186
+
187
+ def _flatten_layers_conf_dict(self, layers_conf, _layers=None):
188
+ """
189
+ Returns a dictionary with all layers that have a name and sources.
190
+ Flattens the layer tree.
191
+ """
192
+ layers = _layers if _layers is not None else odict()
193
+
194
+ if 'layers' in layers_conf:
195
+ for layer in layers_conf.pop('layers'):
196
+ self._flatten_layers_conf_dict(layer, layers)
197
+
198
+ if 'name' in layers_conf and ('sources' in layers_conf or 'tile_sources' in layers_conf):
199
+ layers[layers_conf['name']] = layers_conf
200
+
201
+ return layers
202
+
203
+ def load_wms_root_layer(self):
204
+ self.wms_root_layer = None
205
+
206
+ layers_conf = self._layers_conf_dict()
207
+ if layers_conf is None:
208
+ return
209
+ self.wms_root_layer = WMSLayerConfiguration(layers_conf, context=self)
210
+
211
+ def load_services(self):
212
+ self.services = ServiceConfiguration(self.configuration.get('services', {}), context=self)
213
+
214
+ def configured_services(self):
215
+ with self:
216
+ return self.services.services()
217
+
218
+ def __enter__(self):
219
+ # push local base_config onto config stack
220
+ import mapproxy.config.config
221
+ mapproxy.config.config._config.push(self.base_config)
222
+
223
+ def __exit__(self, type, value, traceback):
224
+ # pop local base_config from config stack
225
+ import mapproxy.config.config
226
+ mapproxy.config.config._config.pop()
227
+
228
+ @property
229
+ def base_config(self):
230
+ return self.globals.base_config
231
+
232
+ def config_files(self):
233
+ """
234
+ Returns a dictionary with all configuration filenames and there timestamps.
235
+ Contains any included files as well (see `base` option).
236
+ """
237
+ return self.configuration.get('__config_files__', {})
238
+
239
+
240
+ def list_of_dicts_to_ordered_dict(dictlist):
241
+ """
242
+ >>> d = list_of_dicts_to_ordered_dict([{'a': 1}, {'b': 2}, {'c': 3}])
243
+ >>> list(d.items())
244
+ [('a', 1), ('b', 2), ('c', 3)]
245
+ """
246
+
247
+ result = odict()
248
+ for d in dictlist:
249
+ for k, v in d.items():
250
+ result[k] = v
251
+ return result
252
+
253
+
254
+ class ConfigurationBase(object):
255
+ """
256
+ Base class for all configurations.
257
+ """
258
+ defaults = {}
259
+
260
+ def __init__(self, conf, context):
261
+ """
262
+ :param conf: the configuration part for this configurator
263
+ :param context: the complete proxy configuration
264
+ :type context: ProxyConfiguration
265
+ """
266
+ self.conf = conf
267
+ self.context = context
268
+ for k, v in self.defaults.items():
269
+ if k not in self.conf:
270
+ self.conf[k] = v
271
+
272
+
273
+ class GridConfiguration(ConfigurationBase):
274
+ @memoize
275
+ def tile_grid(self):
276
+ from mapproxy.grid import tile_grid
277
+
278
+ if 'base' in self.conf:
279
+ base_grid_name = self.conf['base']
280
+ if base_grid_name not in self.context.grids:
281
+ raise ConfigurationError('unknown base %s for grid %s' % (base_grid_name, self.conf['name']))
282
+ conf = self.context.grids[base_grid_name].conf.copy()
283
+ conf.update(self.conf)
284
+ conf.pop('base')
285
+ self.conf = conf
286
+ else:
287
+ conf = self.conf
288
+ align_with = None
289
+ if 'align_resolutions_with' in self.conf:
290
+ align_with_grid_name = self.conf['align_resolutions_with']
291
+ align_with = self.context.grids[align_with_grid_name].tile_grid()
292
+
293
+ tile_size = self.context.globals.get_value('tile_size', conf,
294
+ global_key='grid.tile_size')
295
+ conf['tile_size'] = tuple(tile_size)
296
+ tile_size = tuple(tile_size)
297
+
298
+ stretch_factor = self.context.globals.get_value('stretch_factor', conf,
299
+ global_key='image.stretch_factor')
300
+ max_shrink_factor = self.context.globals.get_value('max_shrink_factor', conf,
301
+ global_key='image.max_shrink_factor')
302
+
303
+ if conf.get('origin') is None:
304
+ log.warning(
305
+ 'grid %s does not have an origin. default origin will change from sw (south/west) to nw (north-west)'
306
+ ' with MapProxy 2.0', conf['name'])
307
+
308
+ grid = tile_grid(
309
+ name=conf['name'],
310
+ srs=conf.get('srs'),
311
+ tile_size=tile_size,
312
+ min_res=conf.get('min_res'),
313
+ max_res=conf.get('max_res'),
314
+ res=conf.get('res'),
315
+ res_factor=conf.get('res_factor', 2.0),
316
+ threshold_res=conf.get('threshold_res'),
317
+ bbox=conf.get('bbox'),
318
+ bbox_srs=conf.get('bbox_srs'),
319
+ num_levels=conf.get('num_levels'),
320
+ stretch_factor=stretch_factor,
321
+ max_shrink_factor=max_shrink_factor,
322
+ align_with=align_with,
323
+ origin=conf.get('origin')
324
+ )
325
+
326
+ return grid
327
+
328
+
329
+ def preferred_srs(conf):
330
+ from mapproxy.srs import SRS, PreferredSrcSRS
331
+
332
+ preferred_conf = conf.get('preferred_src_proj', {})
333
+
334
+ if not preferred_conf:
335
+ return
336
+
337
+ preferred = PreferredSrcSRS()
338
+
339
+ for target, preferred_srcs in preferred_conf.items():
340
+ preferred.add(SRS(target), [SRS(s) for s in preferred_srcs])
341
+
342
+ return preferred
343
+
344
+
345
+ class GlobalConfiguration(ConfigurationBase):
346
+ def __init__(self, conf_base_dir, conf, context):
347
+ ConfigurationBase.__init__(self, conf, context)
348
+ self.base_config = load_default_config()
349
+ self._copy_conf_values(self.conf, self.base_config)
350
+ self.base_config.conf_base_dir = conf_base_dir
351
+ finish_base_config(self.base_config)
352
+
353
+ self.image_options = ImageOptionsConfiguration(self.conf.get('image', {}), context)
354
+ self.preferred_srs = preferred_srs(self.conf.get('srs', {}))
355
+ self.renderd_address = self.get_value('renderd.address')
356
+
357
+ def _copy_conf_values(self, d, target):
358
+ for k, v in d.items():
359
+ if v is None:
360
+ continue
361
+ if (hasattr(v, 'iteritems') or hasattr(v, 'items')) and k in target:
362
+ self._copy_conf_values(v, target[k])
363
+ else:
364
+ target[k] = v
365
+
366
+ def get_value(self, key, local={}, global_key=None, default_key=None):
367
+ result = dotted_dict_get(key, local)
368
+ if result is None:
369
+ result = dotted_dict_get(global_key or key, self.conf)
370
+
371
+ if result is None:
372
+ result = dotted_dict_get(default_key or global_key or key, self.base_config)
373
+
374
+ return result
375
+
376
+ def get_path(self, key, local, global_key=None, default_key=None):
377
+ value = self.get_value(key, local, global_key, default_key)
378
+ if value is not None:
379
+ value = self.abspath(value)
380
+ return value
381
+
382
+ def abspath(self, path):
383
+ return os.path.join(self.base_config.conf_base_dir, path)
384
+
385
+
386
+ default_image_options = {
387
+ }
388
+
389
+
390
+ class ImageOptionsConfiguration(ConfigurationBase):
391
+ def __init__(self, conf, context):
392
+ ConfigurationBase.__init__(self, conf, context)
393
+ self._init_formats()
394
+
395
+ def _init_formats(self):
396
+ self.formats = {}
397
+
398
+ formats_config = default_image_options.copy()
399
+ for format, conf in self.conf.get('formats', {}).items():
400
+ if format in formats_config:
401
+ tmp = formats_config[format].copy()
402
+ tmp.update(conf)
403
+ conf = tmp
404
+ if 'resampling_method' in conf:
405
+ conf['resampling'] = conf.pop('resampling_method')
406
+ if 'encoding_options' in conf:
407
+ self._check_encoding_options(conf['encoding_options'])
408
+ if 'merge_method' in conf:
409
+ warnings.warn('merge_method now defaults to composite. option no longer required',
410
+ DeprecationWarning)
411
+ formats_config[format] = conf
412
+ for format, conf in formats_config.items():
413
+ if 'format' not in conf and format.startswith('image/'):
414
+ conf['format'] = format
415
+ self.formats[format] = conf
416
+
417
+ def _check_encoding_options(self, options):
418
+ if not options:
419
+ return
420
+ options = options.copy()
421
+ jpeg_quality = options.pop('jpeg_quality', None)
422
+ if jpeg_quality and not isinstance(jpeg_quality, int):
423
+ raise ConfigurationError('jpeg_quality is not an integer')
424
+
425
+ tiff_compression = options.pop('tiff_compression', None)
426
+ if tiff_compression and tiff_compression not in ('raw', 'tiff_lzw', 'jpeg'):
427
+ raise ConfigurationError('unknown tiff_compression')
428
+
429
+ quantizer = options.pop('quantizer', None)
430
+ if quantizer and quantizer not in ('fastoctree', 'mediancut'):
431
+ raise ConfigurationError('unknown quantizer')
432
+
433
+ if options:
434
+ raise ConfigurationError('unknown encoding_options: %r' % options)
435
+
436
+ def image_opts(self, image_conf, format):
437
+ from mapproxy.image.opts import ImageOptions
438
+ if not image_conf:
439
+ image_conf = {}
440
+
441
+ conf = {}
442
+ if format in self.formats:
443
+ conf = self.formats[format].copy()
444
+
445
+ resampling = image_conf.get('resampling_method') or conf.get('resampling')
446
+ if resampling is None:
447
+ resampling = self.context.globals.get_value('image.resampling_method', {})
448
+ transparent = image_conf.get('transparent')
449
+ opacity = image_conf.get('opacity')
450
+ img_format = image_conf.get('format')
451
+ colors = image_conf.get('colors')
452
+ mode = image_conf.get('mode')
453
+ encoding_options = image_conf.get('encoding_options')
454
+ if 'merge_method' in image_conf:
455
+ warnings.warn('merge_method now defaults to composite. option no longer required',
456
+ DeprecationWarning)
457
+
458
+ self._check_encoding_options(encoding_options)
459
+
460
+ # only overwrite default if it is not None
461
+ for k, v in dict(
462
+ transparent=transparent, opacity=opacity, resampling=resampling,
463
+ format=img_format, colors=colors, mode=mode, encoding_options=encoding_options,
464
+ ).items():
465
+ if v is not None:
466
+ conf[k] = v
467
+
468
+ if 'format' not in conf and format and format.startswith('image/'):
469
+ conf['format'] = format
470
+
471
+ # caches shall be able to store png and jpeg tiles with mixed format
472
+ if format == 'mixed':
473
+ conf['format'] = format
474
+ conf['transparent'] = True
475
+
476
+ # force 256 colors for image.paletted for backwards compat
477
+ paletted = self.context.globals.get_value('image.paletted', self.conf)
478
+ if conf.get('colors') is None and 'png' in conf.get('format', '') and paletted:
479
+ conf['colors'] = 256
480
+
481
+ opts = ImageOptions(**conf)
482
+ return opts
483
+
484
+
485
+ def dotted_dict_get(key, d):
486
+ """
487
+ >>> dotted_dict_get('foo', {'foo': {'bar': 1}})
488
+ {'bar': 1}
489
+ >>> dotted_dict_get('foo.bar', {'foo': {'bar': 1}})
490
+ 1
491
+ >>> dotted_dict_get('bar', {'foo': {'bar': 1}})
492
+ """
493
+ parts = key.split('.')
494
+ try:
495
+ while parts and d:
496
+ d = d[parts.pop(0)]
497
+ except KeyError:
498
+ return None
499
+ if parts: # not completely resolved
500
+ return None
501
+ return d
502
+
503
+
504
+ class SourcesCollection(dict):
505
+ """
506
+ Collection of SourceConfigurations.
507
+ Allows access to tagged WMS sources, e.g.
508
+ ``sc['source_name:lyr,lyr2']`` will return the source with ``source_name``
509
+ and set ``req.layers`` to ``lyr1,lyr2``.
510
+ """
511
+
512
+ def __getitem__(self, key):
513
+ layers = None
514
+ source_name = key
515
+ if ':' in source_name:
516
+ source_name, layers = source_name.split(':', 1)
517
+ source = dict.__getitem__(self, source_name)
518
+ if not layers:
519
+ return source
520
+
521
+ if source.conf.get('type') not in ('wms', 'mapserver', 'mapnik'):
522
+ raise ConfigurationError("found ':' in: '%s'."
523
+ " tagged sources only supported for WMS/Mapserver/Mapnik" % key)
524
+
525
+ uses_req = source.conf.get('type') != 'mapnik'
526
+
527
+ source = copy(source)
528
+ source.conf = deepcopy(source.conf)
529
+
530
+ if uses_req:
531
+ supported_layers = source.conf['req'].get('layers', [])
532
+ else:
533
+ supported_layers = source.conf.get('layers', [])
534
+ supported_layer_set = SourcesCollection.layer_set(supported_layers)
535
+ layer_set = SourcesCollection.layer_set(layers)
536
+
537
+ if supported_layer_set and not layer_set.issubset(supported_layer_set):
538
+ raise ConfigurationError('layers (%s) not supported by source (%s)' % (
539
+ layers, ','.join(supported_layer_set)))
540
+
541
+ if uses_req:
542
+ source.conf['req']['layers'] = layers
543
+ else:
544
+ source.conf['layers'] = layers
545
+
546
+ return source
547
+
548
+ def __contains__(self, key):
549
+ source_name = key
550
+ if ':' in source_name:
551
+ source_name, _ = source_name.split(':', 1)
552
+ return dict.__contains__(self, source_name)
553
+
554
+ @staticmethod
555
+ def layer_set(layers):
556
+ if isinstance(layers, (list, tuple)):
557
+ return set(layers)
558
+ return set(layers.split(','))
559
+
560
+
561
+ class SourceConfiguration(ConfigurationBase):
562
+
563
+ supports_meta_tiles = True
564
+
565
+ @classmethod
566
+ def load(cls, conf, context):
567
+ source_type = conf['type']
568
+
569
+ subclass = source_configuration_types.get(source_type)
570
+ if not subclass:
571
+ raise ConfigurationError("unknown source type '%s'" % source_type)
572
+
573
+ return subclass(conf, context)
574
+
575
+ @memoize
576
+ def coverage(self):
577
+ if 'coverage' not in self.conf:
578
+ return None
579
+ from mapproxy.config.coverage import load_coverage
580
+ return load_coverage(self.conf['coverage'])
581
+
582
+ def image_opts(self, format=None):
583
+ if 'transparent' in self.conf:
584
+ self.conf.setdefault('image', {})['transparent'] = self.conf['transparent']
585
+ return self.context.globals.image_options.image_opts(self.conf.get('image', {}), format)
586
+
587
+ def supported_srs(self):
588
+ from mapproxy.srs import SRS, SupportedSRS
589
+
590
+ supported_srs = [SRS(code) for code in self.conf.get('supported_srs', [])]
591
+ if not supported_srs:
592
+ return None
593
+ return SupportedSRS(supported_srs, self.context.globals.preferred_srs)
594
+
595
+ def http_client(self, url):
596
+ from mapproxy.client.http import auth_data_from_url, HTTPClient
597
+
598
+ http_client = None
599
+ url, (username, password) = auth_data_from_url(url)
600
+ insecure = ssl_ca_certs = None
601
+ if 'https' in url:
602
+ insecure = self.context.globals.get_value('http.ssl_no_cert_checks', self.conf)
603
+ ssl_ca_certs = self.context.globals.get_path('http.ssl_ca_certs', self.conf)
604
+
605
+ timeout = self.context.globals.get_value('http.client_timeout', self.conf)
606
+ headers = self.context.globals.get_value('http.headers', self.conf)
607
+ hide_error_details = self.context.globals.get_value('http.hide_error_details', self.conf)
608
+ manage_cookies = self.context.globals.get_value('http.manage_cookies', self.conf)
609
+
610
+ http_client = HTTPClient(url, username, password, insecure=insecure,
611
+ ssl_ca_certs=ssl_ca_certs, timeout=timeout,
612
+ headers=headers, hide_error_details=hide_error_details,
613
+ manage_cookies=manage_cookies)
614
+ return http_client, url
615
+
616
+ @memoize
617
+ def on_error_handler(self):
618
+ if 'on_error' not in self.conf:
619
+ return None
620
+ from mapproxy.source.error import HTTPSourceErrorHandler
621
+
622
+ error_handler = HTTPSourceErrorHandler()
623
+ for status_code, response_conf in self.conf['on_error'].items():
624
+ if not isinstance(status_code, int) and status_code != 'other':
625
+ raise ConfigurationError("invalid error code %r in on_error", status_code)
626
+ cacheable = response_conf.get('cache', False)
627
+ color = response_conf.get('response', 'transparent')
628
+ authorize_stale = response_conf.get('authorize_stale', False)
629
+ if color == 'transparent':
630
+ color = (255, 255, 255, 0)
631
+ else:
632
+ color = parse_color(color)
633
+ error_handler.add_handler(status_code, color, cacheable, authorize_stale)
634
+
635
+ return error_handler
636
+
637
+
638
+ def resolution_range(conf):
639
+ from mapproxy.grid import resolution_range as _resolution_range
640
+ if 'min_res' in conf or 'max_res' in conf:
641
+ return _resolution_range(min_res=conf.get('min_res'),
642
+ max_res=conf.get('max_res'))
643
+ if 'min_scale' in conf or 'max_scale' in conf:
644
+ return _resolution_range(min_scale=conf.get('min_scale'),
645
+ max_scale=conf.get('max_scale'))
646
+
647
+
648
+ class ArcGISSourceConfiguration(SourceConfiguration):
649
+ source_type = ('arcgis',)
650
+
651
+ def __init__(self, conf, context):
652
+ SourceConfiguration.__init__(self, conf, context)
653
+
654
+ def source(self, params=None):
655
+ from mapproxy.client.arcgis import ArcGISClient
656
+ from mapproxy.source.arcgis import ArcGISSource
657
+ from mapproxy.request.arcgis import create_request
658
+
659
+ if not self.conf.get('opts', {}).get('map', True):
660
+ return None
661
+
662
+ if not self.context.seed and self.conf.get('seed_only'):
663
+ from mapproxy.source import DummySource
664
+ return DummySource(coverage=self.coverage())
665
+
666
+ supported_formats = [file_ext(f) for f in self.conf.get("supported_formats", [])]
667
+
668
+ # Construct the parameters
669
+ if params is None:
670
+ params = {}
671
+
672
+ request_format = self.conf['req'].get('format')
673
+ if request_format:
674
+ params['format'] = request_format
675
+
676
+ request = create_request(self.conf["req"], params)
677
+ http_client, request.url = self.http_client(request.url)
678
+ coverage = self.coverage()
679
+ res_range = resolution_range(self.conf)
680
+
681
+ client = ArcGISClient(request, http_client)
682
+ image_opts = self.image_opts(format=params.get('format'))
683
+ return ArcGISSource(client, image_opts=image_opts, coverage=coverage,
684
+ res_range=res_range,
685
+ supported_srs=self.supported_srs(),
686
+ supported_formats=supported_formats or None,
687
+ error_handler=self.on_error_handler())
688
+
689
+ @memoize
690
+ def fi_source(self, params=None):
691
+ from mapproxy.client.arcgis import ArcGISInfoClient
692
+ from mapproxy.request.arcgis import create_identify_request
693
+ from mapproxy.source.arcgis import ArcGISInfoSource
694
+
695
+ if params is None:
696
+ params = {}
697
+ request_format = self.conf['req'].get('format')
698
+ if request_format:
699
+ params['format'] = request_format
700
+ fi_source = None
701
+ if self.conf.get('opts', {}).get('featureinfo', False):
702
+ opts = self.conf['opts']
703
+ tolerance = opts.get('featureinfo_tolerance', 5)
704
+ return_geometries = opts.get('featureinfo_return_geometries', False)
705
+
706
+ fi_request = create_identify_request(self.conf['req'], params)
707
+
708
+ http_client, fi_request.url = self.http_client(fi_request.url)
709
+ fi_client = ArcGISInfoClient(fi_request,
710
+ supported_srs=self.supported_srs(),
711
+ http_client=http_client,
712
+ tolerance=tolerance,
713
+ return_geometries=return_geometries,
714
+ )
715
+ fi_source = ArcGISInfoSource(fi_client)
716
+ return fi_source
717
+
718
+
719
+ class WMSSourceConfiguration(SourceConfiguration):
720
+ source_type = ('wms',)
721
+
722
+ @staticmethod
723
+ def static_legend_source(url, context):
724
+ from mapproxy.cache.legend import LegendCache
725
+ from mapproxy.client.wms import WMSLegendURLClient
726
+ from mapproxy.source.wms import WMSLegendSource
727
+
728
+ cache_dir = os.path.join(context.globals.get_path('cache.base_dir', {}),
729
+ 'legends')
730
+ if url.startswith('file://') and not url.startswith('file:///'):
731
+ prefix = 'file://'
732
+ url = prefix + context.globals.abspath(url[7:])
733
+ lg_client = WMSLegendURLClient(url)
734
+ legend_cache = LegendCache(cache_dir=cache_dir)
735
+ return WMSLegendSource([lg_client], legend_cache, static=True)
736
+
737
+ def fi_xslt_transformer(self, conf, context):
738
+ from mapproxy.featureinfo import XSLTransformer, has_xslt_support
739
+ fi_transformer = None
740
+ fi_xslt = conf.get('featureinfo_xslt')
741
+ if fi_xslt:
742
+ if not has_xslt_support:
743
+ raise ValueError('featureinfo_xslt requires lxml. Please install.')
744
+ fi_xslt = context.globals.abspath(fi_xslt)
745
+ fi_format = conf.get('featureinfo_out_format')
746
+ if not fi_format:
747
+ fi_format = conf.get('featureinfo_format')
748
+ fi_transformer = XSLTransformer(fi_xslt, fi_format)
749
+ return fi_transformer
750
+
751
+ def image_opts(self, format=None):
752
+ if 'transparent' not in (self.conf.get('image') or {}):
753
+ transparent = self.conf['req'].get('transparent')
754
+ if transparent is not None:
755
+ transparent = bool(str(transparent).lower() == 'true')
756
+ self.conf.setdefault('image', {})['transparent'] = transparent
757
+ return SourceConfiguration.image_opts(self, format=format)
758
+
759
+ def source(self, params=None):
760
+ from mapproxy.client.wms import WMSClient
761
+ from mapproxy.request.wms import create_request
762
+ from mapproxy.source.wms import WMSSource
763
+
764
+ if not self.conf.get('wms_opts', {}).get('map', True):
765
+ return None
766
+
767
+ if not self.context.seed and self.conf.get('seed_only'):
768
+ from mapproxy.source import DummySource
769
+ return DummySource(coverage=self.coverage())
770
+
771
+ if params is None:
772
+ params = {}
773
+
774
+ request_format = self.conf['req'].get('format')
775
+ if request_format:
776
+ params['format'] = request_format
777
+
778
+ image_opts = self.image_opts(format=params.get('format'))
779
+
780
+ supported_formats = [file_ext(f) for f in self.conf.get('supported_formats', [])]
781
+ version = self.conf.get('wms_opts', {}).get('version', '1.1.1')
782
+
783
+ lock = None
784
+ concurrent_requests = self.context.globals.get_value('concurrent_requests', self.conf,
785
+ global_key='http.concurrent_requests')
786
+ if concurrent_requests:
787
+ from mapproxy.util.lock import SemLock
788
+ lock_dir = self.context.globals.get_path('cache.lock_dir', self.conf)
789
+ lock_timeout = self.context.globals.get_value('http.client_timeout', self.conf)
790
+ url = urlparse(self.conf['req']['url'])
791
+ md5 = hashlib.new('md5', url.netloc.encode('ascii'), usedforsecurity=False)
792
+ lock_file = os.path.join(lock_dir, md5.hexdigest() + '.lck')
793
+ lock = lambda: SemLock(lock_file, concurrent_requests, timeout=lock_timeout) # noqa
794
+
795
+ coverage = self.coverage()
796
+ res_range = resolution_range(self.conf)
797
+
798
+ transparent_color = (self.conf.get('image') or {}).get('transparent_color')
799
+ transparent_color_tolerance = self.context.globals.get_value(
800
+ 'image.transparent_color_tolerance', self.conf)
801
+ if transparent_color:
802
+ transparent_color = parse_color(transparent_color)
803
+
804
+ http_method = self.context.globals.get_value('http.method', self.conf)
805
+
806
+ fwd_req_params = set(self.conf.get('forward_req_params', []))
807
+
808
+ request = create_request(self.conf['req'], params, version=version,
809
+ abspath=self.context.globals.abspath)
810
+ http_client, request.url = self.http_client(request.url)
811
+ client = WMSClient(request, http_client=http_client,
812
+ http_method=http_method, lock=lock,
813
+ fwd_req_params=fwd_req_params)
814
+ return WMSSource(client, image_opts=image_opts, coverage=coverage,
815
+ res_range=res_range, transparent_color=transparent_color,
816
+ transparent_color_tolerance=transparent_color_tolerance,
817
+ supported_srs=self.supported_srs(),
818
+ supported_formats=supported_formats or None,
819
+ fwd_req_params=fwd_req_params,
820
+ error_handler=self.on_error_handler())
821
+
822
+ def fi_source(self, params=None):
823
+ from mapproxy.client.wms import WMSInfoClient
824
+ from mapproxy.request.wms import create_request
825
+ from mapproxy.source.wms import WMSInfoSource
826
+
827
+ if params is None:
828
+ params = {}
829
+ request_format = self.conf['req'].get('format')
830
+ if request_format:
831
+ params['format'] = request_format
832
+ fi_source = None
833
+ if self.conf.get('wms_opts', {}).get('featureinfo', False):
834
+ wms_opts = self.conf['wms_opts']
835
+ version = wms_opts.get('version', '1.1.1')
836
+ if 'featureinfo_format' in wms_opts:
837
+ params['info_format'] = wms_opts['featureinfo_format']
838
+ fi_request = create_request(self.conf['req'], params,
839
+ req_type='featureinfo', version=version,
840
+ abspath=self.context.globals.abspath)
841
+
842
+ fi_transformer = self.fi_xslt_transformer(self.conf.get('wms_opts', {}),
843
+ self.context)
844
+
845
+ http_client, fi_request.url = self.http_client(fi_request.url)
846
+ fi_client = WMSInfoClient(fi_request, supported_srs=self.supported_srs(),
847
+ http_client=http_client)
848
+ coverage = self.coverage()
849
+ fi_source = WMSInfoSource(fi_client, fi_transformer=fi_transformer,
850
+ coverage=coverage)
851
+ return fi_source
852
+
853
+ def lg_source(self, params=None):
854
+ from mapproxy.cache.legend import LegendCache
855
+ from mapproxy.client.wms import WMSLegendClient
856
+ from mapproxy.request.wms import create_request
857
+ from mapproxy.source.wms import WMSLegendSource
858
+
859
+ if params is None:
860
+ params = {}
861
+ request_format = self.conf['req'].get('format')
862
+ if request_format:
863
+ params['format'] = request_format
864
+ lg_source = None
865
+ cache_dir = os.path.join(self.context.globals.get_path('cache.base_dir', {}),
866
+ 'legends')
867
+
868
+ if self.conf.get('wms_opts', {}).get('legendurl', False):
869
+ lg_url = self.conf.get('wms_opts', {}).get('legendurl')
870
+ lg_source = WMSSourceConfiguration.static_legend_source(lg_url, self.context)
871
+ elif self.conf.get('wms_opts', {}).get('legendgraphic', False):
872
+ version = self.conf.get('wms_opts', {}).get('version', '1.1.1')
873
+ lg_req = self.conf['req'].copy()
874
+ lg_clients = []
875
+ lg_layers = str(lg_req['layers']).split(',')
876
+ del lg_req['layers']
877
+ for lg_layer in lg_layers:
878
+ lg_req['layer'] = lg_layer
879
+ lg_request = create_request(lg_req, params,
880
+ req_type='legendgraphic', version=version,
881
+ abspath=self.context.globals.abspath)
882
+ http_client, lg_request.url = self.http_client(lg_request.url)
883
+ lg_client = WMSLegendClient(lg_request, http_client=http_client)
884
+ lg_clients.append(lg_client)
885
+ legend_cache = LegendCache(cache_dir=cache_dir)
886
+ lg_source = WMSLegendSource(lg_clients, legend_cache)
887
+ return lg_source
888
+
889
+
890
+ class MapServerSourceConfiguration(WMSSourceConfiguration):
891
+ source_type = ('mapserver',)
892
+
893
+ def __init__(self, conf, context):
894
+ WMSSourceConfiguration.__init__(self, conf, context)
895
+ self.script = self.context.globals.get_path('mapserver.binary',
896
+ self.conf)
897
+ if not self.script:
898
+ self.script = find_exec('mapserv')
899
+
900
+ if not self.script or not os.path.isfile(self.script):
901
+ raise ConfigurationError('could not find mapserver binary (%r)' %
902
+ (self.script, ))
903
+
904
+ # set url to dummy script name, required as identifier
905
+ # for concurrent_request
906
+ self.conf['req']['url'] = 'mapserver://' + self.script
907
+
908
+ mapfile = self.context.globals.abspath(self.conf['req']['map'])
909
+ self.conf['req']['map'] = mapfile
910
+
911
+ def http_client(self, url):
912
+ working_dir = self.context.globals.get_path('mapserver.working_dir', self.conf)
913
+ if working_dir and not os.path.isdir(working_dir):
914
+ raise ConfigurationError('could not find mapserver working_dir (%r)' % (working_dir, ))
915
+
916
+ from mapproxy.client.cgi import CGIClient
917
+ client = CGIClient(script=self.script, working_directory=working_dir)
918
+ return client, url
919
+
920
+
921
+ class MapnikSourceConfiguration(SourceConfiguration):
922
+ source_type = ('mapnik',)
923
+
924
+ def source(self, params=None):
925
+ if not self.context.seed and self.conf.get('seed_only'):
926
+ from mapproxy.source import DummySource
927
+ return DummySource(coverage=self.coverage())
928
+
929
+ image_opts = self.image_opts()
930
+
931
+ lock = None
932
+ concurrent_requests = self.context.globals.get_value('concurrent_requests', self.conf,
933
+ global_key='http.concurrent_requests')
934
+ if concurrent_requests:
935
+ from mapproxy.util.lock import SemLock
936
+ lock_dir = self.context.globals.get_path('cache.lock_dir', self.conf)
937
+ mapfile = self.conf['mapfile']
938
+ md5 = hashlib.new('md5', mapfile.encode('utf-8'), usedforsecurity=False)
939
+ lock_file = os.path.join(lock_dir, md5.hexdigest() + '.lck')
940
+ lock = lambda: SemLock(lock_file, concurrent_requests) # noqa
941
+
942
+ coverage = self.coverage()
943
+ res_range = resolution_range(self.conf)
944
+
945
+ scale_factor = self.conf.get('scale_factor', None)
946
+ multithreaded = self.conf.get('multithreaded', False)
947
+
948
+ layers = self.conf.get('layers', None)
949
+ if isinstance(layers, str):
950
+ layers = layers.split(',')
951
+
952
+ mapfile = self.context.globals.abspath(self.conf['mapfile'])
953
+
954
+ if self.conf.get('use_mapnik2', False):
955
+ warnings.warn('use_mapnik2 option is no longer needed for Mapnik 2 support',
956
+ DeprecationWarning)
957
+
958
+ from mapproxy.source.mapnik import MapnikSource, mapnik as mapnik_api
959
+ if mapnik_api is None:
960
+ raise ConfigurationError('Could not import Mapnik, please verify it is installed!')
961
+
962
+ if self.context.renderd:
963
+ # only renderd guarantees that we have a single proc/thread
964
+ # that accesses the same mapnik map object
965
+ reuse_map_objects = True
966
+ else:
967
+ reuse_map_objects = False
968
+
969
+ return MapnikSource(mapfile, layers=layers, image_opts=image_opts,
970
+ coverage=coverage, res_range=res_range, lock=lock,
971
+ reuse_map_objects=reuse_map_objects, scale_factor=scale_factor,
972
+ multithreaded=multithreaded)
973
+
974
+
975
+ class TileSourceConfiguration(SourceConfiguration):
976
+ supports_meta_tiles = False
977
+ source_type = ('tile',)
978
+ defaults = {}
979
+
980
+ def source(self, params=None):
981
+ from mapproxy.client.tile import TileClient, TileURLTemplate
982
+ from mapproxy.source.tile import TiledSource
983
+
984
+ if not self.context.seed and self.conf.get('seed_only'):
985
+ from mapproxy.source import DummySource
986
+ return DummySource(coverage=self.coverage())
987
+
988
+ if params is None:
989
+ params = {}
990
+
991
+ url = self.conf['url']
992
+
993
+ if self.conf.get('origin'):
994
+ warnings.warn('origin for tile sources is deprecated since 1.3.0 '
995
+ 'and will be ignored. use grid with correct origin.', RuntimeWarning)
996
+
997
+ http_client, url = self.http_client(url)
998
+
999
+ grid_name = self.conf.get('grid')
1000
+ if grid_name is None:
1001
+ log.warning(
1002
+ "tile source for %s does not have a grid configured and defaults to GLOBAL_MERCATOR. default will"
1003
+ " change with MapProxy 2.0", url)
1004
+ grid_name = "GLOBAL_MERCATOR"
1005
+
1006
+ grid = self.context.grids[grid_name].tile_grid()
1007
+ coverage = self.coverage()
1008
+ res_range = resolution_range(self.conf)
1009
+
1010
+ image_opts = self.image_opts()
1011
+ error_handler = self.on_error_handler()
1012
+
1013
+ format = file_ext(params['format'])
1014
+ client = TileClient(TileURLTemplate(url, format=format), http_client=http_client, grid=grid)
1015
+ return TiledSource(grid, client, coverage=coverage, image_opts=image_opts,
1016
+ error_handler=error_handler, res_range=res_range)
1017
+
1018
+
1019
+ def file_ext(mimetype):
1020
+ from mapproxy.request.base import split_mime_type
1021
+ _mime_class, format, _options = split_mime_type(mimetype)
1022
+ return format
1023
+
1024
+
1025
+ class DebugSourceConfiguration(SourceConfiguration):
1026
+ source_type = ('debug',)
1027
+ required_keys = set('type'.split())
1028
+
1029
+ def source(self, params=None):
1030
+ from mapproxy.source import DebugSource
1031
+ return DebugSource()
1032
+
1033
+
1034
+ source_configuration_types = {
1035
+ 'wms': WMSSourceConfiguration,
1036
+ 'arcgis': ArcGISSourceConfiguration,
1037
+ 'tile': TileSourceConfiguration,
1038
+ 'debug': DebugSourceConfiguration,
1039
+ 'mapserver': MapServerSourceConfiguration,
1040
+ 'mapnik': MapnikSourceConfiguration,
1041
+ }
1042
+
1043
+
1044
+ def register_source_configuration(config_name, config_class,
1045
+ yaml_spec_source_name=None, yaml_spec_source_def=None):
1046
+ """ Method used by plugins to register a new source configuration.
1047
+
1048
+ :param config_name: Name of the source configuration
1049
+ :type config_name: str
1050
+ :param config_class: Class of the source configuration
1051
+ :type config_name: SourceConfiguration
1052
+ :param yaml_spec_source_name: Name of the source in the YAML configuration file
1053
+ :type yaml_spec_source_name: str
1054
+ :param yaml_spec_source_def: Definition of the source in the YAML configuration file
1055
+ :type yaml_spec_source_def: dict
1056
+
1057
+ Example:
1058
+ register_source_configuration('hips', HIPSSourceConfiguration,
1059
+ 'hips', { required('url'): str(),
1060
+ 'resampling_method': str(),
1061
+ 'image': image_opts,
1062
+ })
1063
+ """
1064
+ log.info('Registering configuration for plugin source %s' % config_name)
1065
+ source_configuration_types[config_name] = config_class
1066
+ if yaml_spec_source_name is not None and yaml_spec_source_def is not None:
1067
+ add_source_to_mapproxy_yaml_spec(yaml_spec_source_name, yaml_spec_source_def)
1068
+
1069
+
1070
+ class CacheConfiguration(ConfigurationBase):
1071
+ defaults = {'format': 'image/png'}
1072
+
1073
+ @memoize
1074
+ def coverage(self):
1075
+ if 'cache' not in self.conf or 'coverage' not in self.conf['cache']:
1076
+ return None
1077
+ from mapproxy.config.coverage import load_coverage
1078
+ return load_coverage(self.conf['cache']['coverage'])
1079
+
1080
+ @memoize
1081
+ def cache_dir(self):
1082
+ cache_dir = self.conf.get('cache', {}).get('directory')
1083
+ if cache_dir:
1084
+ if self.conf.get('cache_dir'):
1085
+ log.warning('found cache.directory and cache_dir option for %s, ignoring cache_dir',
1086
+ self.conf['name'])
1087
+ return self.context.globals.abspath(cache_dir)
1088
+
1089
+ return self.context.globals.get_path('cache_dir', self.conf,
1090
+ global_key='cache.base_dir')
1091
+
1092
+ @memoize
1093
+ def has_multiple_grids(self):
1094
+ return len(self.grid_confs()) > 1
1095
+
1096
+ def lock_dir(self):
1097
+ lock_dir = self.context.globals.get_path('cache.tile_lock_dir', self.conf)
1098
+ if not lock_dir:
1099
+ lock_dir = os.path.join(self.cache_dir(), 'tile_locks')
1100
+ return lock_dir
1101
+
1102
+ def _file_cache(self, grid_conf, image_opts):
1103
+ from mapproxy.cache.file import FileCache
1104
+
1105
+ cache_dir = self.cache_dir()
1106
+ directory_layout = self.conf.get('cache', {}).get('directory_layout', 'tc')
1107
+ coverage = self.coverage()
1108
+
1109
+ if self.conf.get('cache', {}).get('directory'):
1110
+ if self.has_multiple_grids():
1111
+ raise ConfigurationError(
1112
+ "using single directory for cache with multiple grids in %s" %
1113
+ (self.conf['name']),
1114
+ )
1115
+ pass
1116
+ elif self.conf.get('cache', {}).get('use_grid_names'):
1117
+ cache_dir = os.path.join(cache_dir, self.conf['name'], grid_conf.tile_grid().name)
1118
+ else:
1119
+ suffix = grid_conf.conf['srs'].replace(':', '')
1120
+ cache_dir = os.path.join(cache_dir, self.conf['name'] + '_' + suffix)
1121
+ link_single_color_images = self.context.globals.get_value('link_single_color_images', self.conf,
1122
+ global_key='cache.link_single_color_images')
1123
+
1124
+ if link_single_color_images and sys.platform == 'win32':
1125
+ log.warning('link_single_color_images not supported on windows')
1126
+ link_single_color_images = False
1127
+
1128
+ return FileCache(
1129
+ cache_dir,
1130
+ file_ext=image_opts.format.ext,
1131
+ image_opts=image_opts,
1132
+ directory_layout=directory_layout,
1133
+ link_single_color_images=link_single_color_images,
1134
+ coverage=coverage
1135
+ )
1136
+
1137
+ def _mbtiles_cache(self, grid_conf, image_opts):
1138
+ from mapproxy.cache.mbtiles import MBTilesCache
1139
+
1140
+ filename = self.conf['cache'].get('filename')
1141
+ if not filename:
1142
+ filename = self.conf['name'] + '.mbtiles'
1143
+
1144
+ if filename.startswith('.' + os.sep):
1145
+ mbfile_path = self.context.globals.abspath(filename)
1146
+ else:
1147
+ mbfile_path = os.path.join(self.cache_dir(), filename)
1148
+
1149
+ sqlite_timeout = self.context.globals.get_value('cache.sqlite_timeout', self.conf)
1150
+ wal = self.context.globals.get_value('cache.sqlite_wal', self.conf)
1151
+ coverage = self.coverage()
1152
+
1153
+ return MBTilesCache(
1154
+ mbfile_path,
1155
+ timeout=sqlite_timeout,
1156
+ wal=wal,
1157
+ coverage=coverage
1158
+ )
1159
+
1160
+ def _geopackage_cache(self, grid_conf, image_opts):
1161
+ from mapproxy.cache.geopackage import GeopackageCache, GeopackageLevelCache
1162
+
1163
+ filename = self.conf['cache'].get('filename')
1164
+ table_name = self.conf['cache'].get('table_name') or \
1165
+ "{}_{}".format(self.conf['name'], grid_conf.tile_grid().name)
1166
+ levels = self.conf['cache'].get('levels')
1167
+ coverage = self.coverage()
1168
+
1169
+ if not filename:
1170
+ filename = self.conf['name'] + '.gpkg'
1171
+ if filename.startswith('.' + os.sep):
1172
+ gpkg_file_path = self.context.globals.abspath(filename)
1173
+ else:
1174
+ gpkg_file_path = os.path.join(self.cache_dir(), filename)
1175
+
1176
+ cache_dir = self.conf['cache'].get('directory')
1177
+ if cache_dir:
1178
+ cache_dir = os.path.join(
1179
+ self.context.globals.abspath(cache_dir),
1180
+ grid_conf.tile_grid().name
1181
+ )
1182
+ else:
1183
+ cache_dir = self.cache_dir()
1184
+ cache_dir = os.path.join(
1185
+ cache_dir,
1186
+ self.conf['name'],
1187
+ grid_conf.tile_grid().name
1188
+ )
1189
+
1190
+ if levels:
1191
+ return GeopackageLevelCache(
1192
+ cache_dir, grid_conf.tile_grid(), table_name, coverage=coverage
1193
+ )
1194
+ else:
1195
+ return GeopackageCache(
1196
+ gpkg_file_path, grid_conf.tile_grid(), table_name, coverage=coverage
1197
+ )
1198
+
1199
+ def _azureblob_cache(self, grid_conf, image_opts):
1200
+ from mapproxy.cache.azureblob import AzureBlobCache
1201
+
1202
+ container_name = self.context.globals.get_value('cache.container_name', self.conf,
1203
+ global_key='cache.azureblob.container_name')
1204
+ coverage = self.coverage()
1205
+
1206
+ if not container_name:
1207
+ raise ConfigurationError("no container_name configured for Azure Blob cache %s" % self.conf['name'])
1208
+
1209
+ connection_string = os.getenv("AZURE_STORAGE_CONNECTION_STRING", self.context.globals.get_value(
1210
+ 'cache.connection_string', self.conf, global_key='cache.azureblob.connection_string'))
1211
+
1212
+ if not connection_string:
1213
+ raise ConfigurationError("no connection_string configured for Azure Blob cache %s" % self.conf['name'])
1214
+
1215
+ directory_layout = self.conf['cache'].get('directory_layout', 'tms')
1216
+
1217
+ base_path = self.conf['cache'].get('directory', None)
1218
+ if base_path is None:
1219
+ base_path = os.path.join(self.conf['name'], grid_conf.tile_grid().name)
1220
+
1221
+ return AzureBlobCache(
1222
+ base_path=base_path,
1223
+ file_ext=image_opts.format.ext,
1224
+ directory_layout=directory_layout,
1225
+ container_name=container_name,
1226
+ connection_string=connection_string,
1227
+ coverage=coverage
1228
+ )
1229
+
1230
+ def _s3_cache(self, grid_conf, image_opts):
1231
+ from mapproxy.cache.s3 import S3Cache
1232
+
1233
+ bucket_name = self.context.globals.get_value('cache.bucket_name', self.conf,
1234
+ global_key='cache.s3.bucket_name')
1235
+ coverage = self.coverage()
1236
+
1237
+ if not bucket_name:
1238
+ raise ConfigurationError("no bucket_name configured for s3 cache %s" % self.conf['name'])
1239
+
1240
+ profile_name = self.context.globals.get_value('cache.profile_name', self.conf,
1241
+ global_key='cache.s3.profile_name')
1242
+
1243
+ region_name = self.context.globals.get_value('cache.region_name', self.conf,
1244
+ global_key='cache.s3.region_name')
1245
+
1246
+ endpoint_url = self.context.globals.get_value('cache.endpoint_url', self.conf,
1247
+ global_key='cache.s3.endpoint_url')
1248
+
1249
+ access_control_list = self.context.globals.get_value('cache.access_control_list', self.conf,
1250
+ global_key='cache.s3.access_control_list')
1251
+
1252
+ use_http_get = self.context.globals.get_value('cache.use_http_get', self.conf,
1253
+ global_key='cache.s3.use_http_get'
1254
+ )
1255
+
1256
+ directory_layout = self.conf['cache'].get('directory_layout', 'tms')
1257
+
1258
+ base_path = self.conf['cache'].get('directory', None)
1259
+ if base_path is None:
1260
+ base_path = os.path.join(self.conf['name'], grid_conf.tile_grid().name)
1261
+
1262
+ return S3Cache(
1263
+ base_path=base_path,
1264
+ file_ext=image_opts.format.ext,
1265
+ directory_layout=directory_layout,
1266
+ bucket_name=bucket_name,
1267
+ profile_name=profile_name,
1268
+ region_name=region_name,
1269
+ endpoint_url=endpoint_url,
1270
+ access_control_list=access_control_list,
1271
+ coverage=coverage,
1272
+ use_http_get=use_http_get
1273
+ )
1274
+
1275
+ def _sqlite_cache(self, grid_conf, image_opts):
1276
+ from mapproxy.cache.mbtiles import MBTilesLevelCache
1277
+
1278
+ cache_dir = self.conf.get('cache', {}).get('directory')
1279
+ if cache_dir:
1280
+ cache_dir = os.path.join(
1281
+ self.context.globals.abspath(cache_dir),
1282
+ grid_conf.tile_grid().name
1283
+ )
1284
+ else:
1285
+ cache_dir = self.cache_dir()
1286
+ cache_dir = os.path.join(
1287
+ cache_dir,
1288
+ self.conf['name'],
1289
+ grid_conf.tile_grid().name
1290
+ )
1291
+
1292
+ sqlite_timeout = self.context.globals.get_value('cache.sqlite_timeout', self.conf)
1293
+ wal = self.context.globals.get_value('cache.sqlite_wal', self.conf)
1294
+ coverage = self.coverage()
1295
+
1296
+ return MBTilesLevelCache(
1297
+ cache_dir,
1298
+ timeout=sqlite_timeout,
1299
+ wal=wal,
1300
+ ttl=self.conf.get('cache', {}).get('ttl', 0),
1301
+ coverage=coverage
1302
+ )
1303
+
1304
+ def _couchdb_cache(self, grid_conf, image_opts):
1305
+ from mapproxy.cache.couchdb import CouchDBCache, CouchDBMDTemplate
1306
+
1307
+ db_name = self.conf['cache'].get('db_name')
1308
+ if not db_name:
1309
+ suffix = grid_conf.conf['srs'].replace(':', '')
1310
+ db_name = self.conf['name'] + '_' + suffix
1311
+
1312
+ url = self.conf['cache'].get('url')
1313
+ if not url:
1314
+ url = 'http://127.0.0.1:5984'
1315
+
1316
+ md_template = CouchDBMDTemplate(self.conf['cache'].get('tile_metadata', {}))
1317
+ tile_id = self.conf['cache'].get('tile_id')
1318
+ coverage = self.coverage()
1319
+
1320
+ return CouchDBCache(url=url, db_name=db_name,
1321
+ file_ext=image_opts.format.ext, tile_grid=grid_conf.tile_grid(),
1322
+ md_template=md_template, tile_id_template=tile_id, coverage=coverage)
1323
+
1324
+ def _riak_cache(self, grid_conf, image_opts):
1325
+ from mapproxy.cache.riak import RiakCache
1326
+
1327
+ default_ports = self.conf['cache'].get('default_ports', {})
1328
+ default_pb_port = default_ports.get('pb', 8087)
1329
+ default_http_port = default_ports.get('http', 8098)
1330
+ coverage = self.coverage()
1331
+
1332
+ nodes = self.conf['cache'].get('nodes')
1333
+ if not nodes:
1334
+ nodes = [{'host': '127.0.0.1'}]
1335
+
1336
+ for n in nodes:
1337
+ if 'pb_port' not in n:
1338
+ n['pb_port'] = default_pb_port
1339
+ if 'http_port' not in n:
1340
+ n['http_port'] = default_http_port
1341
+
1342
+ protocol = self.conf['cache'].get('protocol', 'pbc')
1343
+ bucket = self.conf['cache'].get('bucket')
1344
+ if not bucket:
1345
+ suffix = grid_conf.tile_grid().name
1346
+ bucket = self.conf['name'] + '_' + suffix
1347
+
1348
+ use_secondary_index = self.conf['cache'].get('secondary_index', False)
1349
+ timeout = self.context.globals.get_value('http.client_timeout', self.conf)
1350
+
1351
+ return RiakCache(nodes=nodes, protocol=protocol, bucket=bucket,
1352
+ tile_grid=grid_conf.tile_grid(),
1353
+ use_secondary_index=use_secondary_index,
1354
+ timeout=timeout,
1355
+ coverage=coverage
1356
+ )
1357
+
1358
+ def _redis_cache(self, grid_conf, image_opts):
1359
+ from mapproxy.cache.redis import RedisCache
1360
+
1361
+ host = self.conf['cache'].get('host', '127.0.0.1')
1362
+ port = self.conf['cache'].get('port', 6379)
1363
+ db = self.conf['cache'].get('db', 0)
1364
+ ttl = self.conf['cache'].get('default_ttl', 3600)
1365
+ username = self.conf['cache'].get('username', None)
1366
+ password = self.conf['cache'].get('password', None)
1367
+ coverage = self.coverage()
1368
+ ssl_certfile = self.conf['cache'].get('ssl_certfile', None)
1369
+ ssl_keyfile = self.conf['cache'].get('ssl_keyfile', None)
1370
+ ssl_ca_certs = self.conf['cache'].get('ssl_ca_certs', None)
1371
+ prefix = self.conf['cache'].get('prefix')
1372
+ if not prefix:
1373
+ prefix = self.conf['name'] + '_' + grid_conf.tile_grid().name
1374
+
1375
+ return RedisCache(
1376
+ host=host,
1377
+ port=port,
1378
+ db=db,
1379
+ username=username,
1380
+ password=password,
1381
+ prefix=prefix,
1382
+ ttl=ttl,
1383
+ coverage=coverage,
1384
+ ssl_certfile=ssl_certfile,
1385
+ ssl_keyfile=ssl_keyfile,
1386
+ ssl_ca_certs=ssl_ca_certs
1387
+ )
1388
+
1389
+ def _compact_cache(self, grid_conf, image_opts):
1390
+ from mapproxy.cache.compact import CompactCacheV1, CompactCacheV2
1391
+
1392
+ coverage = self.coverage()
1393
+ cache_dir = self.cache_dir()
1394
+ if self.conf.get('cache', {}).get('directory'):
1395
+ if self.has_multiple_grids():
1396
+ raise ConfigurationError(
1397
+ "using single directory for cache with multiple grids in %s" %
1398
+ (self.conf['name']),
1399
+ )
1400
+ pass
1401
+ else:
1402
+ cache_dir = os.path.join(cache_dir, self.conf['name'], grid_conf.tile_grid().name)
1403
+
1404
+ version = self.conf['cache']['version']
1405
+ if version == 1:
1406
+ return CompactCacheV1(cache_dir=cache_dir, coverage=coverage)
1407
+ elif version == 2:
1408
+ return CompactCacheV2(cache_dir=cache_dir, coverage=coverage)
1409
+
1410
+ raise ConfigurationError("compact cache only supports version 1 or 2")
1411
+
1412
+ def _tile_cache(self, grid_conf, image_opts):
1413
+ if self.conf.get('disable_storage', False):
1414
+ from mapproxy.cache.dummy import DummyCache
1415
+ return DummyCache()
1416
+
1417
+ grid_conf.tile_grid() # create to resolve `base` in grid_conf.conf
1418
+ cache_type = self.conf.get('cache', {}).get('type', 'file')
1419
+ return getattr(self, '_%s_cache' % cache_type)(grid_conf, image_opts)
1420
+
1421
+ def _tile_filter(self):
1422
+ filters = []
1423
+ if 'watermark' in self.conf:
1424
+ from mapproxy.tilefilter import create_watermark_filter
1425
+ if self.conf['watermark'].get('color'):
1426
+ self.conf['watermark']['color'] = parse_color(self.conf['watermark']['color'])
1427
+ f = create_watermark_filter(self.conf, self.context)
1428
+ if f:
1429
+ filters.append(f)
1430
+ return filters
1431
+
1432
+ @memoize
1433
+ def image_opts(self):
1434
+ from mapproxy.image.opts import ImageFormat
1435
+
1436
+ format = None
1437
+ if 'format' not in self.conf.get('image', {}):
1438
+ format = self.conf.get('format') or self.conf.get('request_format')
1439
+ image_opts = self.context.globals.image_options.image_opts(self.conf.get('image', {}), format)
1440
+ if image_opts.format is None:
1441
+ if format is not None and format.startswith('image/'):
1442
+ image_opts.format = ImageFormat(format)
1443
+ else:
1444
+ image_opts.format = ImageFormat('image/png')
1445
+ return image_opts
1446
+
1447
+ def supports_tiled_only_access(self, params=None, tile_grid=None):
1448
+ caches = self.caches()
1449
+ if len(caches) > 1:
1450
+ return False
1451
+
1452
+ cache_grid, extent, tile_manager = caches[0]
1453
+ image_opts = self.image_opts()
1454
+
1455
+ if (tile_grid.is_subset_of(cache_grid)
1456
+ and params.get('format') == image_opts.format):
1457
+ return True
1458
+
1459
+ return False
1460
+
1461
+ def source(self, params=None, tile_grid=None, tiled_only=False):
1462
+ from mapproxy.source.tile import CacheSource
1463
+ from mapproxy.layer import map_extent_from_grid
1464
+
1465
+ caches = self.caches()
1466
+ if len(caches) > 1:
1467
+ # cache with multiple grids/sources
1468
+ source = self.map_layer()
1469
+ source.supports_meta_tiles = True
1470
+ return source
1471
+
1472
+ cache_grid, extent, tile_manager = caches[0]
1473
+ image_opts = self.image_opts()
1474
+
1475
+ cache_extent = map_extent_from_grid(tile_grid)
1476
+ cache_extent = extent.intersection(cache_extent)
1477
+
1478
+ source = CacheSource(tile_manager, extent=cache_extent,
1479
+ image_opts=image_opts, tiled_only=tiled_only)
1480
+ return source
1481
+
1482
+ def _sources_for_grid(self, source_names, grid_conf, request_format):
1483
+ sources = []
1484
+ source_image_opts = []
1485
+
1486
+ # a cache can directly access source tiles when _all_ sources are caches too
1487
+ # and when they have compatible grids by using tiled_only on the CacheSource
1488
+ # check if all sources support tiled_only
1489
+ tiled_only = True
1490
+ for source_name in source_names:
1491
+ if source_name in self.context.sources:
1492
+ tiled_only = False
1493
+ break
1494
+ elif source_name in self.context.caches:
1495
+ cache_conf = self.context.caches[source_name]
1496
+ tiled_only = cache_conf.supports_tiled_only_access(
1497
+ params={'format': request_format},
1498
+ tile_grid=grid_conf.tile_grid(),
1499
+ )
1500
+ if not tiled_only:
1501
+ break
1502
+
1503
+ for source_name in source_names:
1504
+ if source_name in self.context.sources:
1505
+ source_conf = self.context.sources[source_name]
1506
+ source = source_conf.source({'format': request_format})
1507
+ elif source_name in self.context.caches:
1508
+ cache_conf = self.context.caches[source_name]
1509
+ source = cache_conf.source(
1510
+ params={'format': request_format},
1511
+ tile_grid=grid_conf.tile_grid(),
1512
+ tiled_only=tiled_only,
1513
+ )
1514
+ else:
1515
+ raise ConfigurationError('unknown source %s' % source_name)
1516
+ if source:
1517
+ sources.append(source)
1518
+ source_image_opts.append(source.image_opts)
1519
+
1520
+ return sources, source_image_opts
1521
+
1522
+ def _sources_for_band_merge(self, sources_conf, grid_conf, request_format):
1523
+ from mapproxy.image.merge import BandMerger
1524
+
1525
+ source_names = []
1526
+
1527
+ for band, band_sources in sources_conf.items():
1528
+ for source in band_sources:
1529
+ name = source['source']
1530
+ if name in source_names:
1531
+ idx = source_names.index(name)
1532
+ else:
1533
+ source_names.append(name)
1534
+ idx = len(source_names) - 1
1535
+
1536
+ source["src_idx"] = idx
1537
+
1538
+ sources, source_image_opts = self._sources_for_grid(
1539
+ source_names=source_names,
1540
+ grid_conf=grid_conf,
1541
+ request_format=request_format,
1542
+ )
1543
+
1544
+ if 'l' in sources_conf:
1545
+ mode = 'L'
1546
+ elif 'a' in sources_conf:
1547
+ mode = 'RGBA'
1548
+ else:
1549
+ mode = 'RGB'
1550
+
1551
+ band_merger = BandMerger(mode=mode)
1552
+ available_bands = {'r': 0, 'g': 1, 'b': 2, 'a': 3, 'l': 0}
1553
+ for band, band_sources in sources_conf.items():
1554
+ band_idx = available_bands.get(band)
1555
+ if band_idx is None:
1556
+ raise ConfigurationError("unsupported band '%s' for cache %s"
1557
+ % (band, self.conf['name']))
1558
+ for source in band_sources:
1559
+ band_merger.add_ops(
1560
+ dst_band=band_idx,
1561
+ src_img=source['src_idx'],
1562
+ src_band=source['band'],
1563
+ factor=source.get('factor', 1.0),
1564
+ )
1565
+
1566
+ return band_merger, sources, source_image_opts
1567
+
1568
+ @memoize
1569
+ def caches(self):
1570
+ from mapproxy.cache.dummy import DummyCache, DummyLocker
1571
+ from mapproxy.cache.tile import TileManager
1572
+ from mapproxy.cache.base import TileLocker
1573
+ from mapproxy.image.opts import compatible_image_options
1574
+ from mapproxy.layer import map_extent_from_grid, merge_layer_extents
1575
+
1576
+ base_image_opts = self.image_opts()
1577
+ if (self.conf.get('format') == 'mixed' and
1578
+ self.conf.get('request_format') not in ['image/png', 'image/vnd.jpeg-png']):
1579
+ raise ConfigurationError(
1580
+ 'request_format must be set to image/png or image/vnd.jpeg-png if mixed mode is enabled')
1581
+ request_format = self.conf.get('request_format') or self.conf.get('format')
1582
+ if '/' in request_format:
1583
+ request_format_ext = request_format.split('/', 1)[1]
1584
+ else:
1585
+ request_format_ext = request_format
1586
+
1587
+ caches = []
1588
+
1589
+ meta_buffer = self.context.globals.get_value('meta_buffer', self.conf,
1590
+ global_key='cache.meta_buffer')
1591
+ meta_size = self.context.globals.get_value('meta_size', self.conf,
1592
+ global_key='cache.meta_size')
1593
+ bulk_meta_tiles = self.context.globals.get_value('bulk_meta_tiles', self.conf,
1594
+ global_key='cache.bulk_meta_tiles')
1595
+ minimize_meta_requests = self.context.globals.get_value('minimize_meta_requests', self.conf,
1596
+ global_key='cache.minimize_meta_requests')
1597
+ concurrent_tile_creators = self.context.globals.get_value('concurrent_tile_creators', self.conf,
1598
+ global_key='cache.concurrent_tile_creators')
1599
+
1600
+ cache_rescaled_tiles = self.conf.get('cache_rescaled_tiles')
1601
+ upscale_tiles = self.conf.get('upscale_tiles', 0)
1602
+ if upscale_tiles < 0:
1603
+ raise ConfigurationError("upscale_tiles must be positive")
1604
+ downscale_tiles = self.conf.get('downscale_tiles', 0)
1605
+ if downscale_tiles < 0:
1606
+ raise ConfigurationError("downscale_tiles must be positive")
1607
+ if upscale_tiles and downscale_tiles:
1608
+ raise ConfigurationError("cannot use both upscale_tiles and downscale_tiles")
1609
+
1610
+ rescale_tiles = 0
1611
+ if upscale_tiles:
1612
+ rescale_tiles = -upscale_tiles
1613
+ if downscale_tiles:
1614
+ rescale_tiles = downscale_tiles
1615
+
1616
+ renderd_address = self.context.globals.get_value('renderd.address', self.conf)
1617
+
1618
+ band_merger = None
1619
+ for grid_name, grid_conf in self.grid_confs():
1620
+ if isinstance(self.conf['sources'], dict):
1621
+ band_merger, sources, source_image_opts = self._sources_for_band_merge(
1622
+ self.conf['sources'],
1623
+ grid_conf=grid_conf,
1624
+ request_format=request_format,
1625
+ )
1626
+ else:
1627
+ sources, source_image_opts = self._sources_for_grid(
1628
+ self.conf['sources'],
1629
+ grid_conf=grid_conf,
1630
+ request_format=request_format,
1631
+ )
1632
+
1633
+ if not sources:
1634
+ from mapproxy.source import DummySource
1635
+ sources = [DummySource()]
1636
+ source_image_opts.append(sources[0].image_opts)
1637
+ tile_grid = grid_conf.tile_grid()
1638
+ tile_filter = self._tile_filter()
1639
+ image_opts = compatible_image_options(source_image_opts, base_opts=base_image_opts)
1640
+ cache = self._tile_cache(grid_conf, image_opts)
1641
+ identifier = self.conf['name'] + '_' + tile_grid.name
1642
+
1643
+ tile_creator_class = None
1644
+
1645
+ use_renderd = bool(renderd_address)
1646
+ if self.context.renderd:
1647
+ # we _are_ renderd
1648
+ use_renderd = False
1649
+ if self.conf.get('disable_storage', False):
1650
+ # can't ask renderd to create tiles that shouldn't be cached
1651
+ use_renderd = False
1652
+
1653
+ if use_renderd:
1654
+ from mapproxy.cache.renderd import RenderdTileCreator, has_renderd_support
1655
+ if not has_renderd_support():
1656
+ raise ConfigurationError("renderd requires requests library")
1657
+ if self.context.seed:
1658
+ priority = 10
1659
+ else:
1660
+ priority = 100
1661
+
1662
+ cache_dir = self.cache_dir()
1663
+
1664
+ lock_dir = self.context.globals.get_value('cache.tile_lock_dir')
1665
+ if not lock_dir:
1666
+ lock_dir = os.path.join(cache_dir, 'tile_locks')
1667
+
1668
+ lock_timeout = self.context.globals.get_value('http.client_timeout', {})
1669
+ locker = TileLocker(lock_dir, lock_timeout, identifier + '_renderd')
1670
+ # TODO band_merger
1671
+ tile_creator_class = partial(RenderdTileCreator, renderd_address,
1672
+ priority=priority, tile_locker=locker)
1673
+
1674
+ else:
1675
+ from mapproxy.cache.tile import TileCreator
1676
+ tile_creator_class = partial(TileCreator, image_merger=band_merger)
1677
+
1678
+ if isinstance(cache, DummyCache):
1679
+ locker = DummyLocker()
1680
+ else:
1681
+ locker = TileLocker(
1682
+ lock_dir=self.lock_dir(),
1683
+ lock_timeout=self.context.globals.get_value('http.client_timeout', {}),
1684
+ lock_cache_id=cache.lock_cache_id,
1685
+ )
1686
+
1687
+ mgr = TileManager(tile_grid, cache, sources, image_opts.format.ext,
1688
+ locker=locker,
1689
+ image_opts=image_opts, identifier=identifier,
1690
+ request_format=request_format_ext,
1691
+ meta_size=meta_size, meta_buffer=meta_buffer,
1692
+ minimize_meta_requests=minimize_meta_requests,
1693
+ concurrent_tile_creators=concurrent_tile_creators,
1694
+ pre_store_filter=tile_filter,
1695
+ tile_creator_class=tile_creator_class,
1696
+ bulk_meta_tiles=bulk_meta_tiles,
1697
+ cache_rescaled_tiles=cache_rescaled_tiles,
1698
+ rescale_tiles=rescale_tiles,
1699
+ )
1700
+ if self.conf['name'] in self.context.caches:
1701
+ mgr._refresh_before = self.context.caches[self.conf['name']].conf.get('refresh_before', {})
1702
+ extent = merge_layer_extents(sources)
1703
+ # If the cache has a defined coverage prefer it's extent over source extent
1704
+ if cache.coverage:
1705
+ extent = cache.coverage.extent
1706
+ elif extent.is_default:
1707
+ extent = map_extent_from_grid(tile_grid)
1708
+ caches.append((tile_grid, extent, mgr))
1709
+ return caches
1710
+
1711
+ @memoize
1712
+ def grid_confs(self):
1713
+ grid_names = self.conf.get('grids')
1714
+ if grid_names is None:
1715
+ log.warning(
1716
+ 'cache %s does not have any grids. default will change from [GLOBAL_MERCATOR] to [GLOBAL_WEBMERCATOR]'
1717
+ ' with MapProxy 2.0', self.conf['name'])
1718
+ grid_names = ['GLOBAL_MERCATOR']
1719
+ return [(g, self.context.grids[g]) for g in grid_names]
1720
+
1721
+ @memoize
1722
+ def map_layer(self):
1723
+ from mapproxy.layer import CacheMapLayer, SRSConditional, ResolutionConditional
1724
+
1725
+ image_opts = self.image_opts()
1726
+ max_tile_limit = self.context.globals.get_value('max_tile_limit', self.conf,
1727
+ global_key='cache.max_tile_limit')
1728
+ caches = []
1729
+ main_grid = None
1730
+ for grid, extent, tile_manager in self.caches():
1731
+ if main_grid is None:
1732
+ main_grid = grid
1733
+ caches.append((CacheMapLayer(tile_manager, extent=extent, image_opts=image_opts,
1734
+ max_tile_limit=max_tile_limit),
1735
+ grid.srs))
1736
+
1737
+ if len(caches) == 1:
1738
+ layer = caches[0][0]
1739
+ else:
1740
+ layer = SRSConditional(caches, caches[0][0].extent, opacity=image_opts.opacity,
1741
+ preferred_srs=self.context.globals.preferred_srs)
1742
+
1743
+ if 'use_direct_from_level' in self.conf:
1744
+ self.conf['use_direct_from_res'] = main_grid.resolution(self.conf['use_direct_from_level'])
1745
+ if 'use_direct_from_res' in self.conf:
1746
+ if len(self.conf['sources']) != 1:
1747
+ raise ValueError('use_direct_from_level/res only supports single sources')
1748
+ source_conf = self.context.sources[self.conf['sources'][0]]
1749
+ layer = ResolutionConditional(layer, source_conf.source(), self.conf['use_direct_from_res'],
1750
+ main_grid.srs, layer.extent, opacity=image_opts.opacity)
1751
+ return layer
1752
+
1753
+
1754
+ class WMSLayerConfiguration(ConfigurationBase):
1755
+ @memoize
1756
+ def wms_layer(self):
1757
+ from mapproxy.service.wms import WMSGroupLayer
1758
+
1759
+ layers = []
1760
+ this_layer = None
1761
+
1762
+ if 'layers' in self.conf:
1763
+ layers_conf = self.conf['layers']
1764
+ for layer_conf in layers_conf:
1765
+ lyr = WMSLayerConfiguration(layer_conf, self.context).wms_layer()
1766
+ if lyr:
1767
+ layers.append(lyr)
1768
+
1769
+ if 'sources' in self.conf or 'legendurl' in self.conf:
1770
+ this_layer = LayerConfiguration(self.conf, self.context).wms_layer()
1771
+
1772
+ if not layers and not this_layer:
1773
+ return None
1774
+
1775
+ if not layers:
1776
+ layer = this_layer
1777
+ else:
1778
+ layer = WMSGroupLayer(name=self.conf.get('name'), title=self.conf.get('title'),
1779
+ this=this_layer, layers=layers, md=self.conf.get('md'))
1780
+ return layer
1781
+
1782
+
1783
+ def cache_source_names(context, cache):
1784
+ """
1785
+ Return all sources for a cache, even if a caches uses another cache.
1786
+ """
1787
+ source_names = []
1788
+ for src in context.caches[cache].conf['sources']:
1789
+ if src in context.caches and src not in context.sources:
1790
+ source_names.extend(cache_source_names(context, src))
1791
+ else:
1792
+ source_names.append(src)
1793
+
1794
+ return source_names
1795
+
1796
+
1797
+ class LayerConfiguration(ConfigurationBase):
1798
+ @memoize
1799
+ def wms_layer(self):
1800
+ from mapproxy.service.wms import WMSLayer
1801
+
1802
+ sources = []
1803
+ fi_sources = []
1804
+ lg_sources = []
1805
+
1806
+ lg_sources_configured = False
1807
+ if self.conf.get('legendurl'):
1808
+ legend_url = self.conf['legendurl']
1809
+ lg_sources.append(WMSSourceConfiguration.static_legend_source(legend_url, self.context))
1810
+ lg_sources_configured = True
1811
+
1812
+ for source_name in self.conf.get('sources', []):
1813
+ fi_source_names = []
1814
+ lg_source_names = []
1815
+ if source_name in self.context.caches:
1816
+ map_layer = self.context.caches[source_name].map_layer()
1817
+ fi_source_names = cache_source_names(self.context, source_name)
1818
+ lg_source_names = cache_source_names(self.context, source_name)
1819
+ elif source_name in self.context.sources:
1820
+ source_conf = self.context.sources[source_name]
1821
+ if not source_conf.supports_meta_tiles:
1822
+ raise ConfigurationError('source "%s" of layer "%s" does not support un-tiled access'
1823
+ % (source_name, self.conf.get('name')))
1824
+ map_layer = source_conf.source()
1825
+ fi_source_names = [source_name]
1826
+ lg_source_names = [source_name]
1827
+ else:
1828
+ raise ConfigurationError('source/cache "%s" not found' % source_name)
1829
+
1830
+ if map_layer:
1831
+ sources.append(map_layer)
1832
+
1833
+ for fi_source_name in fi_source_names:
1834
+ if fi_source_name not in self.context.sources:
1835
+ continue
1836
+ if not hasattr(self.context.sources[fi_source_name], 'fi_source'):
1837
+ continue
1838
+ fi_source = self.context.sources[fi_source_name].fi_source()
1839
+ if fi_source:
1840
+ fi_sources.append(fi_source)
1841
+ if not lg_sources_configured:
1842
+ for lg_source_name in lg_source_names:
1843
+ if lg_source_name not in self.context.sources:
1844
+ continue
1845
+ if not hasattr(self.context.sources[lg_source_name], 'lg_source'):
1846
+ continue
1847
+ lg_source = self.context.sources[lg_source_name].lg_source()
1848
+ if lg_source:
1849
+ lg_sources.append(lg_source)
1850
+
1851
+ res_range = resolution_range(self.conf)
1852
+ dimensions = None
1853
+ if 'dimensions' in self.conf.keys():
1854
+ dimensions = self.dimensions()
1855
+
1856
+ layer = WMSLayer(
1857
+ self.conf.get('name'), self.conf.get('title'), sources, fi_sources, lg_sources, res_range=res_range,
1858
+ md=self.conf.get('md'), dimensions=dimensions)
1859
+ return layer
1860
+
1861
+ @memoize
1862
+ def dimensions(self):
1863
+ from mapproxy.layer import Dimension
1864
+ from mapproxy.util.ext.wmsparse.util import parse_datetime_range
1865
+ dimensions = {}
1866
+ for dimension, conf in self.conf.get('dimensions', {}).items():
1867
+ raw_values = conf.get('values')
1868
+ if len(raw_values) == 1:
1869
+ # look for time or dim_reference_time
1870
+ if 'time' in dimension.lower():
1871
+ log.debug('Determining values as datetime strings')
1872
+ values = parse_datetime_range(raw_values[0])
1873
+ else:
1874
+ log.debug('Determining values as plain strings')
1875
+ values = raw_values[0].strip().split('/')
1876
+ else:
1877
+ values = [str(val) for val in conf.get('values', ['default'])]
1878
+
1879
+ default = conf.get('default', values[-1])
1880
+ dimensions[dimension.lower()] = Dimension(dimension, values, default=default)
1881
+ return dimensions
1882
+
1883
+ @memoize
1884
+ def tile_layers(self, grid_name_as_path=False):
1885
+ from mapproxy.service.tile import TileLayer
1886
+ from mapproxy.cache.dummy import DummyCache
1887
+ sources = []
1888
+ fi_only_sources = []
1889
+ if 'tile_sources' in self.conf:
1890
+ sources = self.conf['tile_sources']
1891
+ else:
1892
+ for source_name in self.conf.get('sources', []):
1893
+ # we only support caches for tiled access...
1894
+ if source_name not in self.context.caches:
1895
+ if source_name in self.context.sources:
1896
+ src_conf = self.context.sources[source_name].conf
1897
+ # but we ignore debug layers for convenience
1898
+ if src_conf['type'] == 'debug':
1899
+ continue
1900
+ # and WMS layers with map: False (i.e. FeatureInfo only sources)
1901
+ if src_conf['type'] == 'wms' and src_conf.get('wms_opts', {}).get('map', True) is False:
1902
+ fi_only_sources.append(source_name)
1903
+ continue
1904
+
1905
+ return []
1906
+ sources.append(source_name)
1907
+
1908
+ if len(sources) > 1:
1909
+ # skip layers with more then one source
1910
+ return []
1911
+
1912
+ dimensions = self.dimensions()
1913
+
1914
+ tile_layers = []
1915
+ for cache_name in sources:
1916
+ fi_sources = []
1917
+ fi_source_names = cache_source_names(self.context, cache_name)
1918
+
1919
+ for fi_source_name in fi_source_names + fi_only_sources:
1920
+ if fi_source_name not in self.context.sources:
1921
+ continue
1922
+ if not hasattr(self.context.sources[fi_source_name], 'fi_source'):
1923
+ continue
1924
+ fi_source = self.context.sources[fi_source_name].fi_source()
1925
+ if fi_source:
1926
+ fi_sources.append(fi_source)
1927
+
1928
+ for grid, extent, cache_source in self.context.caches[cache_name].caches():
1929
+ disable_storage = self.context.configuration['caches'][cache_name].get('disable_storage', False)
1930
+ if disable_storage:
1931
+ supports_dimensions = isinstance(cache_source.cache, DummyCache)
1932
+ else:
1933
+ supports_dimensions = cache_source.cache.supports_dimensions
1934
+ if dimensions and not supports_dimensions:
1935
+ # caching of dimension layers is not supported yet
1936
+ raise ConfigurationError(
1937
+ "caching of dimension layer (%s) is not supported yet."
1938
+ " need to `disable_storage: true` on %s cache" % (self.conf['name'], cache_name)
1939
+ )
1940
+
1941
+ md = {}
1942
+ md['title'] = self.conf['title']
1943
+ md['name'] = self.conf['name']
1944
+ md['grid_name'] = grid.name
1945
+ if grid_name_as_path:
1946
+ md['name_path'] = (md['name'], md['grid_name'])
1947
+ else:
1948
+ md['name_path'] = (md['name'], grid.srs.srs_code.replace(':', '').upper())
1949
+ md['name_internal'] = md['name_path'][0] + '_' + md['name_path'][1]
1950
+ md['format'] = self.context.caches[cache_name].image_opts().format
1951
+ md['cache_name'] = cache_name
1952
+ md['extent'] = extent
1953
+ tile_layers.append(
1954
+ TileLayer(
1955
+ self.conf['name'], self.conf['title'],
1956
+ info_sources=fi_sources,
1957
+ md=md,
1958
+ tile_manager=cache_source,
1959
+ dimensions=dimensions,
1960
+ )
1961
+ )
1962
+
1963
+ return tile_layers
1964
+
1965
+
1966
+ def fi_xslt_transformers(conf, context):
1967
+ from mapproxy.featureinfo import XSLTransformer, has_xslt_support
1968
+ fi_transformers = {}
1969
+ fi_xslt = conf.get('featureinfo_xslt')
1970
+ if fi_xslt:
1971
+ if not has_xslt_support:
1972
+ raise ValueError('featureinfo_xslt requires lxml. Please install.')
1973
+ for info_type, fi_xslt in fi_xslt.items():
1974
+ fi_xslt = context.globals.abspath(fi_xslt)
1975
+ fi_transformers[info_type] = XSLTransformer(fi_xslt)
1976
+ return fi_transformers
1977
+
1978
+
1979
+ def extents_for_srs(bbox_srs):
1980
+ from mapproxy.layer import DefaultMapExtent, MapExtent
1981
+ from mapproxy.srs import SRS
1982
+ extents = {}
1983
+ for srs in bbox_srs:
1984
+ if isinstance(srs, str):
1985
+ bbox = DefaultMapExtent()
1986
+ else:
1987
+ srs, bbox = srs['srs'], srs['bbox']
1988
+ bbox = MapExtent(bbox, SRS(srs))
1989
+
1990
+ extents[srs] = bbox
1991
+
1992
+ return extents
1993
+
1994
+
1995
+ plugin_services = {}
1996
+
1997
+
1998
+ def register_service_configuration(service_name, service_creator,
1999
+ yaml_spec_service_name=None, yaml_spec_service_def=None):
2000
+ """ Method used by plugins to register a new service.
2001
+
2002
+ :param service_name: Name of the service
2003
+ :type service_name: str
2004
+ :param service_creator: Creator method of the service
2005
+ :type service_creator: method of type (serviceConfiguration: ServiceConfiguration, conf: dict) -> Server
2006
+ :param yaml_spec_service_name: Name of the service in the YAML configuration file
2007
+ :type yaml_spec_service_name: str
2008
+ :param yaml_spec_service_def: Definition of the service in the YAML configuration file
2009
+ :type yaml_spec_service_def: dict
2010
+ """
2011
+
2012
+ log.info('Registering configuration for plugin service %s' % service_name)
2013
+ plugin_services[service_name] = service_creator
2014
+ if yaml_spec_service_name is not None and yaml_spec_service_def is not None:
2015
+ add_service_to_mapproxy_yaml_spec(yaml_spec_service_name, yaml_spec_service_def)
2016
+
2017
+
2018
+ class ServiceConfiguration(ConfigurationBase):
2019
+ def __init__(self, conf, context):
2020
+ if 'wms' in conf:
2021
+ if conf['wms'] is None:
2022
+ conf['wms'] = {}
2023
+ if 'md' not in conf['wms']:
2024
+ conf['wms']['md'] = {'title': 'MapProxy WMS'}
2025
+
2026
+ ConfigurationBase.__init__(self, conf, context)
2027
+
2028
+ def services(self):
2029
+ services = []
2030
+ ows_services = []
2031
+ for service_name, service_conf in self.conf.items():
2032
+ creator = getattr(self, service_name + '_service', None)
2033
+ if not creator:
2034
+ # If not a known service, try to use the plugin mechanism
2035
+ global plugin_services
2036
+ creator = plugin_services.get(service_name, None)
2037
+ if not creator:
2038
+ raise ValueError('unknown service: %s' % service_name)
2039
+ new_services = creator(self, service_conf or {})
2040
+ else:
2041
+ new_services = creator(service_conf or {})
2042
+
2043
+ # a creator can return a list of services...
2044
+ if not isinstance(new_services, (list, tuple)):
2045
+ new_services = [new_services]
2046
+
2047
+ for new_service in new_services:
2048
+ if getattr(new_service, 'service', None):
2049
+ ows_services.append(new_service)
2050
+ else:
2051
+ services.append(new_service)
2052
+
2053
+ if ows_services:
2054
+ from mapproxy.service.ows import OWSServer
2055
+ services.append(OWSServer(ows_services))
2056
+ return services
2057
+
2058
+ def tile_layers(self, conf, use_grid_names=False):
2059
+ layers = odict()
2060
+ for layer_name, layer_conf in self.context.layers.items():
2061
+ for tile_layer in layer_conf.tile_layers(grid_name_as_path=use_grid_names):
2062
+ if not tile_layer:
2063
+ continue
2064
+ if use_grid_names:
2065
+ layers[tile_layer.md['name_path']] = tile_layer
2066
+ else:
2067
+ layers[tile_layer.md['name_internal']] = tile_layer
2068
+ return layers
2069
+
2070
+ def kml_service(self, conf):
2071
+ from mapproxy.service.kml import KMLServer
2072
+
2073
+ md = self.context.services.conf.get('wms', {}).get('md', {}).copy()
2074
+ md.update(conf.get('md', {}))
2075
+ max_tile_age = self.context.globals.get_value('tiles.expires_hours')
2076
+ max_tile_age *= 60 * 60 # seconds
2077
+ use_grid_names = conf.get('use_grid_names', False)
2078
+ layers = self.tile_layers(conf, use_grid_names=use_grid_names)
2079
+ return KMLServer(layers, md, max_tile_age=max_tile_age, use_dimension_layers=use_grid_names)
2080
+
2081
+ def tms_service(self, conf):
2082
+ from mapproxy.service.tile import TileServer
2083
+
2084
+ md = self.context.services.conf.get('wms', {}).get('md', {}).copy()
2085
+ md.update(conf.get('md', {}))
2086
+ max_tile_age = self.context.globals.get_value('tiles.expires_hours')
2087
+ max_tile_age *= 60 * 60 # seconds
2088
+
2089
+ origin = conf.get('origin')
2090
+ use_grid_names = conf.get('use_grid_names', False)
2091
+ layers = self.tile_layers(conf, use_grid_names=use_grid_names)
2092
+ return TileServer(layers, md, max_tile_age=max_tile_age, use_dimension_layers=use_grid_names,
2093
+ origin=origin)
2094
+
2095
+ def wmts_service(self, conf):
2096
+ from mapproxy.service.wmts import WMTSServer, WMTSRestServer
2097
+
2098
+ md = self.context.services.conf.get('wms', {}).get('md', {}).copy()
2099
+ md.update(conf.get('md', {}))
2100
+ layers = self.tile_layers(conf, use_grid_names=True)
2101
+
2102
+ kvp = conf.get('kvp')
2103
+ restful = conf.get('restful')
2104
+
2105
+ max_tile_age = self.context.globals.get_value('tiles.expires_hours')
2106
+ max_tile_age *= 60 * 60 # seconds
2107
+
2108
+ info_formats = conf.get('featureinfo_formats', [])
2109
+ info_formats = odict((f['suffix'], f['mimetype']) for f in info_formats)
2110
+
2111
+ if kvp is None and restful is None:
2112
+ kvp = restful = True
2113
+
2114
+ services = []
2115
+ if kvp:
2116
+ services.append(
2117
+ WMTSServer(
2118
+ layers, md, max_tile_age=max_tile_age,
2119
+ info_formats=info_formats,
2120
+ )
2121
+ )
2122
+
2123
+ if restful:
2124
+ template = conf.get('restful_template')
2125
+ fi_template = conf.get('restful_featureinfo_template')
2126
+ if template and '{{' in template:
2127
+ # TODO remove warning in 1.6
2128
+ log.warning("double braces in WMTS restful_template are deprecated {{x}} -> {x}")
2129
+ services.append(
2130
+ WMTSRestServer(
2131
+ layers, md, template=template,
2132
+ fi_template=fi_template,
2133
+ max_tile_age=max_tile_age,
2134
+ info_formats=info_formats,
2135
+ )
2136
+ )
2137
+
2138
+ return services
2139
+
2140
+ def wms_service(self, conf):
2141
+ from mapproxy.service.wms import WMSServer
2142
+ from mapproxy.request.wms import Version
2143
+
2144
+ md = conf.get('md', {})
2145
+ inspire_md = conf.get('inspire_md', {})
2146
+ tile_layers = self.tile_layers(conf)
2147
+ attribution = conf.get('attribution')
2148
+ strict = self.context.globals.get_value('strict', conf, global_key='wms.strict')
2149
+ on_source_errors = self.context.globals.get_value('on_source_errors',
2150
+ conf, global_key='wms.on_source_errors')
2151
+ root_layer = self.context.wms_root_layer.wms_layer()
2152
+ if not root_layer:
2153
+ raise ConfigurationError("found no WMS layer")
2154
+ if not root_layer.title:
2155
+ # set title of root layer to WMS title
2156
+ root_layer.title = md.get('title')
2157
+ concurrent_layer_renderer = self.context.globals.get_value(
2158
+ 'concurrent_layer_renderer', conf,
2159
+ global_key='wms.concurrent_layer_renderer')
2160
+ image_formats_names = self.context.globals.get_value('image_formats', conf,
2161
+ global_key='wms.image_formats')
2162
+ image_formats = odict()
2163
+ for format in image_formats_names:
2164
+ opts = self.context.globals.image_options.image_opts({}, format)
2165
+ if opts.format in image_formats:
2166
+ log.warning('duplicate mime-type for WMS image_formats: "%s" already configured, will use last format',
2167
+ opts.format)
2168
+ image_formats[opts.format] = opts
2169
+ info_types = conf.get('featureinfo_types')
2170
+ srs = self.context.globals.get_value('srs', conf, global_key='wms.srs')
2171
+ self.context.globals.base_config.wms.srs = srs
2172
+ srs_extents = extents_for_srs(conf.get('bbox_srs', []))
2173
+
2174
+ versions = conf.get('versions')
2175
+ if versions:
2176
+ versions = sorted([Version(v) for v in versions])
2177
+
2178
+ max_output_pixels = self.context.globals.get_value('max_output_pixels', conf,
2179
+ global_key='wms.max_output_pixels')
2180
+ if isinstance(max_output_pixels, list):
2181
+ max_output_pixels = max_output_pixels[0] * max_output_pixels[1]
2182
+
2183
+ max_tile_age = self.context.globals.get_value('tiles.expires_hours')
2184
+ max_tile_age *= 60 * 60 # seconds
2185
+
2186
+ server = WMSServer(root_layer, md, attribution=attribution,
2187
+ image_formats=image_formats, info_types=info_types,
2188
+ srs=srs, tile_layers=tile_layers, strict=strict, on_error=on_source_errors,
2189
+ concurrent_layer_renderer=concurrent_layer_renderer,
2190
+ max_output_pixels=max_output_pixels, srs_extents=srs_extents,
2191
+ max_tile_age=max_tile_age, versions=versions,
2192
+ inspire_md=inspire_md,
2193
+ )
2194
+
2195
+ server.fi_transformers = fi_xslt_transformers(conf, self.context)
2196
+
2197
+ return server
2198
+
2199
+ def demo_service(self, conf):
2200
+ from mapproxy.service.demo import DemoServer
2201
+ services = list(self.context.services.conf.keys())
2202
+ md = self.context.services.conf.get('wms', {}).get('md', {}).copy()
2203
+ md.update(conf.get('md', {}))
2204
+ layers = odict()
2205
+ for layer_name, layer_conf in self.context.layers.items():
2206
+ lyr = layer_conf.wms_layer()
2207
+ if lyr:
2208
+ layers[layer_name] = lyr
2209
+ image_formats = self.context.globals.get_value('image_formats', conf, global_key='wms.image_formats')
2210
+ srs = self.context.globals.get_value('srs', conf, global_key='wms.srs')
2211
+ tms_conf = self.context.services.conf.get('tms', {}) or {}
2212
+ use_grid_names = tms_conf.get('use_grid_names', False)
2213
+ tile_layers = self.tile_layers(tms_conf, use_grid_names=use_grid_names)
2214
+
2215
+ # WMTS restful template
2216
+ wmts_conf = self.context.services.conf.get('wmts', {}) or {}
2217
+ from mapproxy.service.wmts import WMTSRestServer
2218
+ if wmts_conf:
2219
+ restful_template = wmts_conf.get('restful_template', WMTSRestServer.default_template)
2220
+ else:
2221
+ restful_template = WMTSRestServer.default_template
2222
+
2223
+ if 'wmts' in self.context.services.conf:
2224
+ kvp = wmts_conf.get('kvp')
2225
+ restful = wmts_conf.get('restful')
2226
+
2227
+ if kvp is None and restful is None:
2228
+ kvp = restful = True
2229
+
2230
+ if kvp:
2231
+ services.append('wmts_kvp')
2232
+ if restful:
2233
+ services.append('wmts_restful')
2234
+
2235
+ if 'wms' in self.context.services.conf:
2236
+ versions = self.context.services.conf['wms'].get('versions', ['1.1.1'])
2237
+ if '1.1.1' in versions:
2238
+ # demo service only supports 1.1.1, use wms_111 as an indicator
2239
+ services.append('wms_111')
2240
+
2241
+ layers = odict(sorted(layers.items(), key=lambda x: x[1].name))
2242
+ background = self.context.globals.get_value('background', conf)
2243
+
2244
+ return DemoServer(
2245
+ layers, md, tile_layers=tile_layers, image_formats=image_formats, srs=srs, services=services,
2246
+ restful_template=restful_template, background=background)
2247
+
2248
+
2249
+ def load_plugins():
2250
+ """ Locate plugins that belong to the 'mapproxy' group and load them """
2251
+ try:
2252
+ import importlib.metadata
2253
+ except ImportError:
2254
+ return
2255
+
2256
+ for dist in importlib.metadata.distributions():
2257
+ for ep in dist.entry_points:
2258
+ if ep.group == 'mapproxy':
2259
+ log.info('Loading plugin from package %s' % dist.metadata['name'])
2260
+ ep.load().plugin_entrypoint()
2261
+
2262
+
2263
+ def load_configuration(mapproxy_conf, seed=False, ignore_warnings=True, renderd=False):
2264
+
2265
+ load_plugins()
2266
+
2267
+ conf_base_dir = os.path.abspath(os.path.dirname(mapproxy_conf))
2268
+
2269
+ # A configuration is checked/validated four times, each step has a different
2270
+ # focus and returns different errors. The steps are:
2271
+ # 1. YAML loading: checks YAML syntax like tabs vs. space, indention errors, etc.
2272
+ # 2. Options: checks all options agains the spec and validates their types,
2273
+ # e.g is disable_storage a bool, is layers a list, etc.
2274
+ # 3. References: checks if all referenced caches, sources and grids exist
2275
+ # 4. Initialization: creates all MapProxy objects, returns on first error
2276
+
2277
+ try:
2278
+ conf_dict = load_configuration_file([os.path.basename(mapproxy_conf)], conf_base_dir)
2279
+ except YAMLError as ex:
2280
+ raise ConfigurationError(ex)
2281
+ errors, informal_only = validate_options(conf_dict)
2282
+ for error in errors:
2283
+ log.warning(error)
2284
+ if not informal_only or (errors and not ignore_warnings):
2285
+ raise ConfigurationError('invalid configuration')
2286
+ errors = validate(conf_dict)
2287
+ for error in errors:
2288
+ log.warning(error)
2289
+
2290
+ return ProxyConfiguration(conf_dict, conf_base_dir=conf_base_dir, seed=seed,
2291
+ renderd=renderd)
2292
+
2293
+
2294
+ def load_configuration_file(files, working_dir):
2295
+ """
2296
+ Return configuration dict from imported files
2297
+ """
2298
+ # record all config files with timestamp for reloading
2299
+ conf_dict = {'__config_files__': {}}
2300
+ for conf_file in files:
2301
+ conf_file = os.path.normpath(os.path.join(working_dir, conf_file))
2302
+ log.info('reading: %s' % conf_file)
2303
+ current_dict = load_yaml_file(conf_file)
2304
+ conf_dict['__config_files__'][os.path.abspath(conf_file)] = os.path.getmtime(conf_file)
2305
+ if 'base' in current_dict:
2306
+ current_working_dir = os.path.dirname(conf_file)
2307
+ base_files = current_dict.pop('base')
2308
+ if isinstance(base_files, str):
2309
+ base_files = [base_files]
2310
+ imported_dict = load_configuration_file(base_files, current_working_dir)
2311
+ current_dict = merge_dict(current_dict, imported_dict)
2312
+ conf_dict = merge_dict(conf_dict, current_dict)
2313
+
2314
+ return conf_dict
2315
+
2316
+
2317
+ def merge_dict(conf, base):
2318
+ """
2319
+ Return `base` dict with values from `conf` merged in.
2320
+ """
2321
+ for k, v in conf.items():
2322
+ if k not in base:
2323
+ base[k] = v
2324
+ else:
2325
+ if isinstance(base[k], dict):
2326
+ if v is not None:
2327
+ base[k] = merge_dict(v, base[k])
2328
+ elif isinstance(base[k], list):
2329
+ if v is not None:
2330
+ if k in ['bbox', 'tile_size', 'max_output_pixels', 'sources', 'grids']:
2331
+ base[k] = v
2332
+ elif k in ['layers']:
2333
+ base[k] = merge_layers(v, base[k])
2334
+ elif len(v) == 0: # delete
2335
+ base[k] = None
2336
+ else:
2337
+ base[k] = base[k] + v
2338
+ else:
2339
+ base[k] = v
2340
+ return base
2341
+
2342
+
2343
+ def merge_layers(conf, base):
2344
+ """
2345
+ Return `base` dict with values from `conf` merged in.
2346
+ """
2347
+ out = []
2348
+ remaining_conf = []
2349
+ for conf_layer in conf:
2350
+ remaining_conf.append(conf_layer['name'])
2351
+
2352
+ for base_layer in base:
2353
+ found = False
2354
+ for conf_layer in conf:
2355
+ if conf_layer['name'] in remaining_conf and base_layer['name'] == conf_layer['name']:
2356
+ new_layer = merge_dict(conf_layer, base_layer)
2357
+ out.append(new_layer)
2358
+ remaining_conf.remove(conf_layer['name'])
2359
+ found = True
2360
+ break
2361
+
2362
+ if not found:
2363
+ out.append(base_layer)
2364
+
2365
+ for conf_layer in conf:
2366
+ if conf_layer['name'] in remaining_conf:
2367
+ out.append(conf_layer)
2368
+
2369
+ return out
2370
+
2371
+
2372
+ def parse_color(color):
2373
+ """
2374
+ >>> parse_color((100, 12, 55))
2375
+ (100, 12, 55)
2376
+ >>> parse_color('0xff0530')
2377
+ (255, 5, 48)
2378
+ >>> parse_color('#FF0530')
2379
+ (255, 5, 48)
2380
+ >>> parse_color('#FF053080')
2381
+ (255, 5, 48, 128)
2382
+ """
2383
+ if isinstance(color, (list, tuple)) and 3 <= len(color) <= 4:
2384
+ return tuple(color)
2385
+ if not isinstance(color, str):
2386
+ raise ValueError('color needs to be a tuple/list or 0xrrggbb/#rrggbb(aa) string, got %r' % color)
2387
+
2388
+ if color.startswith('0x'):
2389
+ color = color[2:]
2390
+ if color.startswith('#'):
2391
+ color = color[1:]
2392
+
2393
+ r, g, b = map(lambda x: int(x, 16), [color[:2], color[2:4], color[4:6]])
2394
+
2395
+ if len(color) == 8:
2396
+ a = int(color[6:8], 16)
2397
+ return r, g, b, a
2398
+
2399
+ return r, g, b