Compare commits
1131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cfd0ced73 | |||
| 1464a337e3 | |||
| 1eaba084ed | |||
| 37ba7457d5 | |||
| 43a7c058fd | |||
| 87c7c7ff93 | |||
| 8c8241b13b | |||
| c187f02c27 | |||
| 617adcdc48 | |||
| e71c49e38c | |||
| 9572eefd48 | |||
| 3303a25737 | |||
| 49f91a69f1 | |||
| c853c754b1 | |||
| 1b5596052c | |||
| 1c1b5cd208 | |||
| c948814efc | |||
| 7b9d3512c6 | |||
| edf3a58ea6 | |||
| 5a4b5fb9e9 | |||
| 6f9f53add6 | |||
| e0199c9ce7 | |||
| e1ed42f16f | |||
| 104c3c0c10 | |||
| f4a1aa4729 | |||
| fda7fac260 | |||
| 87cf81d1c9 | |||
| 8e35ed97e6 | |||
| 124ae98bf3 | |||
| b784bf2b84 | |||
| 09a1f3f87b | |||
| a22476cab2 | |||
| a324ddb654 | |||
| 656264f2bc | |||
| 974dd7bfd9 | |||
| fa182e66fa | |||
| 3bf0e510e6 | |||
| e8d07fa694 | |||
| 4d1e2e6893 | |||
| f219ff1e17 | |||
| 8eabfa5519 | |||
| ad69cbdb20 | |||
| 17eaa3f204 | |||
| 1df2e36dbb | |||
| 0c46affda9 | |||
| 18f45e932b | |||
| 31d56c9fae | |||
| b7838ad6e1 | |||
| c92fd12854 | |||
| eb55796461 | |||
| 4b97f82980 | |||
| 5d0b6e3201 | |||
| 5ab9be9fdb | |||
| 9659d41b83 | |||
| 52f949e152 | |||
| e4d559f953 | |||
| 577663f706 | |||
| 393f4a38b9 | |||
| 3727c9ad5e | |||
| a99cbc00ba | |||
| fe5f7e8b8c | |||
| 3ed4b8f2d7 | |||
| f17c2e8112 | |||
| 2be447f105 | |||
| 2fe6fa2be6 | |||
| 83dd21414b | |||
| 56d96a248d | |||
| 5b1ee7296b | |||
| 8e0546fba4 | |||
| 2ff3d08bb0 | |||
| afe7a492a7 | |||
| 1f26ab707f | |||
| 233191e27c | |||
| 246f9a191d | |||
| 07f6efe45d | |||
| ccd0402eea | |||
| 8ccee0ba75 | |||
| d6b67be7b2 | |||
| ac56ea4627 | |||
| b00cf66ea6 | |||
| 8e329a0ff9 | |||
| 74cacd5bdf | |||
| 7d5fb23130 | |||
| 08bbd55f13 | |||
| d6d4930513 | |||
| 6220e15723 | |||
| 72053a2dba | |||
| bf27a35ef5 | |||
| 4705bf823a | |||
| a8f6208561 | |||
| af2629be75 | |||
| 8bfad6c9fd | |||
| 5c3713da90 | |||
| 174eae1cf4 | |||
| d73add77e7 | |||
| ab00c9f426 | |||
| d6ab302815 | |||
| f5646f57d1 | |||
| c77d8f6625 | |||
| e34c1ca4be | |||
| 87745f1fc9 | |||
| 9ba0fd5f39 | |||
| 70decbb5c1 | |||
| 51fa4a4377 | |||
| fb6f4d73e2 | |||
| 63b14933ba | |||
| a86c31cefa | |||
| 25e2554412 | |||
| 34bb3bc0a7 | |||
| 852243cd52 | |||
| 40faf86adf | |||
| bb69f143b8 | |||
| bb43200887 | |||
| ae3f79f4ad | |||
| 731a315624 | |||
| 5e441454d5 | |||
| a55716bbd9 | |||
| 44aea3745e | |||
| a572bc02a6 | |||
| 4dee95fa2e | |||
| 381ff6292f | |||
| c12db1703b | |||
| 2676583833 | |||
| 6ca2af7f8a | |||
| 553b17822d | |||
| a6148e5f40 | |||
| 4f77629eb7 | |||
| 9103d9560a | |||
| 06c63509f7 | |||
| bbe0af6c0e | |||
| 7455ccc3b7 | |||
| 8b766777ac | |||
| 8441abe907 | |||
| ca56951256 | |||
| 5981151b27 | |||
| 21dbc6c4d9 | |||
| 6f060c2a0a | |||
| 24327541c5 | |||
| 0d6b9ee9d4 | |||
| 3ad487dce7 | |||
| a46c834874 | |||
| 67bdea1827 | |||
| cc4133aac1 | |||
| 4a6f58d61c | |||
| e3dc98b76d | |||
| 09373dda44 | |||
| c4b05b62b3 | |||
| 06bccfab10 | |||
| b83c9a1ad9 | |||
| 0279daf7bd | |||
| bfada7cace | |||
| 6c751fe1c4 | |||
| 34299c94ee | |||
| 5804b3c72a | |||
| 8ae6e9fc61 | |||
| 40579c3e0c | |||
| f3906c8dd8 | |||
| f911ed3522 | |||
| 2cc179acc1 | |||
| 2912db6e32 | |||
| 70eb3e785a | |||
| 8f944d9973 | |||
| 8faf841f3d | |||
| d8ba8fcbfb | |||
| d6f31ec82e | |||
| b19eacf4f9 | |||
| 3d158a791a | |||
| 04b3b42524 | |||
| af92aac7ec | |||
| 90dfbf37e0 | |||
| e21f412933 | |||
| da297fe82c | |||
| 624ccb5b00 | |||
| 9f0e5641ab | |||
| d07b40ccef | |||
| e0abd8251d | |||
| 324452e714 | |||
| 8d3ad6b1a1 | |||
| 3d4cc34a08 | |||
| ff351716b6 | |||
| c963a8cc59 | |||
| 52ad980aef | |||
| fb04cce650 | |||
| 4b8acb10b0 | |||
| 86d67389fc | |||
| 9dad53e444 | |||
| d5afeb14c1 | |||
| 85bb1d4d40 | |||
| d95e25d185 | |||
| a88f2bc0a3 | |||
| 5a4254d300 | |||
| 6bfe2aa364 | |||
| 0acd1fec0a | |||
| 3046fedb55 | |||
| 1574b49b01 | |||
| 4417a2d8f1 | |||
| 1f34e927e7 | |||
| 9af6a537ef | |||
| 6e07142f13 | |||
| d2853f34a4 | |||
| 7faed0d27e | |||
| 22101f93ad | |||
| 0661899d54 | |||
| 862ac84464 | |||
| 1e4aadbb6d | |||
| ce731e7d6b | |||
| 5448cdbe4e | |||
| 315a9b82c0 | |||
| 9e1c0e4c82 | |||
| 7db675e0d1 | |||
| 0aa629164a | |||
| c2ec77e869 | |||
| b28407a4a0 | |||
| e3e6647e03 | |||
| eb0f833d49 | |||
| c5c1dd12b9 | |||
| b7e414a042 | |||
| 81a6f31803 | |||
| 65cd050a18 | |||
| c426dd7349 | |||
| c5af645546 | |||
| 1bf0abfb5a | |||
| 18338de21a | |||
| 857117eb71 | |||
| 8d86bca8b3 | |||
| dc4e6a0925 | |||
| e9d6119ac6 | |||
| cbdf45c852 | |||
| a191c36f96 | |||
| 0a8d0974ca | |||
| 8e6c54564b | |||
| 4bbe2ce1ea | |||
| 1d48ab7ecf | |||
| d85ed36e70 | |||
| b554f4427b | |||
| 184bfeeaf8 | |||
| e45f8d960d | |||
| 1fede3af3a | |||
| 5ad74048bd | |||
| a0a91474de | |||
| c3670701e5 | |||
| 86923330d9 | |||
| 4517148e5a | |||
| 0433d66f45 | |||
| 0c592bacab | |||
| d98637cb12 | |||
| 1070209cb5 | |||
| b6189f77c4 | |||
| 25322a3952 | |||
| 5943c6f282 | |||
| 090a26bb40 | |||
| af03b010a4 | |||
| 578a920b6d | |||
| 0858a16ffc | |||
| 7cfe0563bc | |||
| ee72389534 | |||
| 8f722c59de | |||
| 94d2e958eb | |||
| d931c7cb38 | |||
| 6da83c4670 | |||
| d26af16003 | |||
| d75b86c927 | |||
| a0cd087ccc | |||
| 313b639a5f | |||
| 93c45d5340 | |||
| 3be6cb5926 | |||
| 9d151fdc1f | |||
| 44d1a7a6b0 | |||
| 0e5676eae0 | |||
| b8739915b2 | |||
| a1dedd1aa6 | |||
| 33b4a40c35 | |||
| a624909819 | |||
| d5d387f349 | |||
| c58ae0f895 | |||
| c9e360d675 | |||
| 75ece350bd | |||
| 45680151ef | |||
| 87bae82b17 | |||
| fcad636f85 | |||
| bac8495b3c | |||
| df98cb9c7b | |||
| 34c3015b42 | |||
| 7d51ea9b25 | |||
| ca7bf9f4a7 | |||
| 61ba399e1c | |||
| b5c09a4f17 | |||
| fcfc7ab874 | |||
| a0bc4744a2 | |||
| e6e4c928f3 | |||
| 00388e03b8 | |||
| 6f83839a4c | |||
| 74b47e7e74 | |||
| f70d078ec6 | |||
| 1d8131fb85 | |||
| 8c82db9162 | |||
| f23f60807a | |||
| 8f581956e8 | |||
| 87fb0f6a5c | |||
| 1c953bc296 | |||
| ce976991d4 | |||
| be7642976b | |||
| a5b226d9f3 | |||
| 1b3d8b5b79 | |||
| 97a61afc24 | |||
| d1f6bc95db | |||
| 10b03c4e00 | |||
| 449d8e7665 | |||
| 4b8800e8ac | |||
| 874c6460f6 | |||
| 55cb86f721 | |||
| a30017fd2c | |||
| 3b5ba6c59e | |||
| a693f6db4e | |||
| 5be7e0c7ec | |||
| 7c6b3c0d80 | |||
| e5e03be382 | |||
| 0d35106cc8 | |||
| 5710edcff7 | |||
| 2cc7c63bb1 | |||
| ba9d21c045 | |||
| 4ef6ecb7eb | |||
| 52e8b21b3d | |||
| c6ba4f66e2 | |||
| 5a47a96f09 | |||
| ae878f6000 | |||
| aebfe62a8e | |||
| 6be3543616 | |||
| 0f6708d9d4 | |||
| 0705522a24 | |||
| 35098111ac | |||
| 39333953d7 | |||
| adea3d0f13 | |||
| d1cb2690fc | |||
| 3ffdcc9639 | |||
| 793b5d15ad | |||
| cff32696cc | |||
| 029eff9317 | |||
| b7d08dfaa6 | |||
| 94089836bf | |||
| ff8a8fb749 | |||
| 1300951a29 | |||
| 3cb1ed9403 | |||
| a5ac1cc93d | |||
| 29a0ef0d1c | |||
| 26907f7014 | |||
| 3ac06c7cb1 | |||
| f268f09ca2 | |||
| f4c18fd91b | |||
| 0567b098ec | |||
| dc6006fd6d | |||
| 109261ba00 | |||
| b539644b2b | |||
| be36eec93d | |||
| fe5066a2a4 | |||
| 642e5c0b29 | |||
| 97bdc93a39 | |||
| c770b440c2 | |||
| 7559985a2e | |||
| 532bbecb47 | |||
| 8ed67e98ce | |||
| 5792c632f9 | |||
| 3e2927e6a4 | |||
| f09cc25a3b | |||
| c5c3e93aff | |||
| cc371d6a70 | |||
| 1b17e8fa0a | |||
| bc4706de83 | |||
| 0f22eb83d3 | |||
| a8cbe19f09 | |||
| c714e23d79 | |||
| 5b3d579f87 | |||
| 725b752e35 | |||
| 223721a65d | |||
| 9b4d924dff | |||
| b31537e063 | |||
| 63ed8c1de3 | |||
| 7aa0298f7c | |||
| 62f3cbe8fb | |||
| 30facc885f | |||
| 17aa88e3b6 | |||
| 5b9af07ebc | |||
| 6b45dc8b4d | |||
| 0009c74948 | |||
| 8911f83ef3 | |||
| 2fafafe0dc | |||
| 27e6675d26 | |||
| 4269e4573c | |||
| 096e2b6aec | |||
| 33e04b3527 | |||
| 79fa2b3508 | |||
| d5ef412300 | |||
| 0726a494be | |||
| 926969b921 | |||
| 59e070f463 | |||
| 2ccd1d227e | |||
| 655877f67e | |||
| 6c240d53e4 | |||
| f89f8ed4ea | |||
| 6123b464de | |||
| 49dba02e8f | |||
| fb49a3abe5 | |||
| c88f9ab5dc | |||
| d886b14d09 | |||
| bd1204a7a5 | |||
| 9cadda0236 | |||
| 90ea6323c1 | |||
| 51f2cfac16 | |||
| 4dbb423ac2 | |||
| a3ee1cc27e | |||
| fea0798349 | |||
| bd8abffa28 | |||
| a5f3a43cde | |||
| 6c5dc7e06b | |||
| b1c8a12e88 | |||
| 401c6971f4 | |||
| 7e5a5ce077 | |||
| 6b245c9894 | |||
| b963fe9619 | |||
| 673887d93b | |||
| 06898429fd | |||
| 0196ba4eb4 | |||
| ef81534a17 | |||
| a958ec943b | |||
| 4e3b395b3d | |||
| 5e7fd4f93c | |||
| 25cad5bb25 | |||
| f9c230414e | |||
| 866f8d034a | |||
| be6aa559fb | |||
| a560176d83 | |||
| 4644e78fd2 | |||
| 237cc16b97 | |||
| dffa54afb0 | |||
| 225e5c48e4 | |||
| 2e017d252a | |||
| e728e5f7e4 | |||
| f0371b41b1 | |||
| a51442921a | |||
| f39fb34f36 | |||
| 566201fb45 | |||
| 88841b56e7 | |||
| 5aa0b4e7d9 | |||
| f19fc4a8a1 | |||
| cd162309a8 | |||
| aead867e27 | |||
| 663f295623 | |||
| c588164190 | |||
| d61d0a5795 | |||
| dddd604f7b | |||
| ea3b9a20c5 | |||
| 7415b8af08 | |||
| d06e053d34 | |||
| 7075a8b05e | |||
| 4cbcf14588 | |||
| ca202d7701 | |||
| 8dfc16e7ee | |||
| fbf5cec670 | |||
| 14d4383f8a | |||
| 58bdd39f9e | |||
| ab9ab7acc7 | |||
| be39fd2ec8 | |||
| 3c0185da27 | |||
| b37b7276fb | |||
| c45cf2f0c8 | |||
| 1f03fdbb50 | |||
| f3b8c5362a | |||
| c9a5dd01be | |||
| 0fa4d40e92 | |||
| 8a6e64c8c2 | |||
| 72b6dd1ae9 | |||
| ee525631fa | |||
| ee9e055af3 | |||
| b214c6ac7e | |||
| eb75020861 | |||
| a44e757e31 | |||
| 9ac908948d | |||
| 19e82e5890 | |||
| bf84fd24ee | |||
| affeb7c751 | |||
| 9743361e0d | |||
| ab16120af2 | |||
| 37e5ba0fff | |||
| 0aa0dad7fb | |||
| 2910efde6e | |||
| eac7656786 | |||
| be3175beae | |||
| 26de95a263 | |||
| d0a47bd122 | |||
| 8c760bb810 | |||
| c27deefdef | |||
| 392a845460 | |||
| e7622c2080 | |||
| 3a558412ba | |||
| 95e205943a | |||
| eb8686325c | |||
| 1f77e156e6 | |||
| 92ee50a4a4 | |||
| ef23f01e67 | |||
| 22b6a4a2bf | |||
| 201ecac156 | |||
| 563a78ed42 | |||
| 47acc2640b | |||
| f088788246 | |||
| e219dcd332 | |||
| b8829d9a5c | |||
| 2c83c976c6 | |||
| d63782ddf2 | |||
| 3eabcbec72 | |||
| 00ab303e44 | |||
| 38bf12701e | |||
| e4ec1d155a | |||
| 361f083687 | |||
| c1a59200e2 | |||
| 6e0432ff5e | |||
| 1c83de08c1 | |||
| 0af828543b | |||
| 369cc23a30 | |||
| db56ad8b2e | |||
| 7fa17d81ac | |||
| 019c6a0086 | |||
| c1bee74b57 | |||
| b794279304 | |||
| 935dfa1704 | |||
| bda7a0e659 | |||
| 8d1cc340b8 | |||
| 338c6b59a8 | |||
| 021f8ab400 | |||
| f305db9e3e | |||
| e916b25594 | |||
| 5f1e212759 | |||
| 2b7db498ef | |||
| e6464790f6 | |||
| 13ddf9f754 | |||
| 30edb881ed | |||
| b30bbdc248 | |||
| 824616f6bd | |||
| 2a832955c4 | |||
| 608b836fe0 | |||
| de9c4fcc4a | |||
| 109198a524 | |||
| 920e4fe630 | |||
| 5e143e0a8e | |||
| 57f803d52c | |||
| c55d342c7e | |||
| e9065635cd | |||
| 6057721249 | |||
| 975487d271 | |||
| 46b2fd5978 | |||
| f61313449f | |||
| 366ad4d7df | |||
| b5cfb44cf0 | |||
| 050cc9cea9 | |||
| b2f194eeee | |||
| 97b0b8541d | |||
| b5eb74fe20 | |||
| 0500172d42 | |||
| 0e7bd98485 | |||
| ff0ece5149 | |||
| db9ad86ac2 | |||
| a066710bfb | |||
| 52740483b6 | |||
| 518a624e20 | |||
| 4ba71c8bd5 | |||
| ceeb628784 | |||
| 2ec6a90dc3 | |||
| 4e37d834ed | |||
| a7922d894d | |||
| eeda3296ab | |||
| acd26e0162 | |||
| fbf828e202 | |||
| af9015f529 | |||
| 7a172b2022 | |||
| a609dc4029 | |||
| 92bfee4bcc | |||
| 559d4618d1 | |||
| 5c391ee287 | |||
| db74cfeb2a | |||
| 726b825e4c | |||
| 84d56b2606 | |||
| e9a8b094a2 | |||
| 924b14621a | |||
| b072e3a98c | |||
| 827bd5fa24 | |||
| 9e0410afe6 | |||
| ef08a9347e | |||
| 9b732540a6 | |||
| 24c52074b8 | |||
| cb6c6e0d9f | |||
| 884dc6fa49 | |||
| efe42021f1 | |||
| 470277c253 | |||
| c1cab38c7a | |||
| 1cf36ddb08 | |||
| 1fec89b69e | |||
| 911549aca3 | |||
| 41329ec2f8 | |||
| 15cdfbc980 | |||
| 5053058c32 | |||
| 8a8cfad303 | |||
| cc3c17078d | |||
| 47965d5f57 | |||
| c947d9e3ed | |||
| 8318180e96 | |||
| 87daf6fb76 | |||
| 5d254ac2ff | |||
| 482f322d9f | |||
| b1d097a40f | |||
| da456b08fe | |||
| 2776ac3ce0 | |||
| eb2fc4c715 | |||
| d2ffc3a0b1 | |||
| 40a9978f31 | |||
| 22688933b3 | |||
| 7cf01f0c12 | |||
| 1e87765f95 | |||
| cc8fe4e02e | |||
| 979fc98e70 | |||
| c3c0c35d8a | |||
| f9913cad63 | |||
| 0ed64f94e8 | |||
| 1deecd4e2a | |||
| e0e9201b46 | |||
| 362c7b437e | |||
| efd0b547e9 | |||
| afbaaa66bc | |||
| de8c687dfb | |||
| 1bb079f078 | |||
| b35322522f | |||
| 6a605571e0 | |||
| c4ec77c911 | |||
| f7643cee7e | |||
| cd95202dcc | |||
| facba3998b | |||
| 1f9cc2ce33 | |||
| ad505378ab | |||
| fb7b30c81d | |||
| cb3f93c67d | |||
| 76cb5a6b7c | |||
| 990e47cb24 | |||
| 8be7465428 | |||
| e2b7a6a517 | |||
| 284e00b665 | |||
| 1fbfa8428b | |||
| b13b89c7ce | |||
| 3fa31e2a2e | |||
| 0e7d2d8ff5 | |||
| 2def3820dd | |||
| e1a489f318 | |||
| 760839eb83 | |||
| 7136d2dea3 | |||
| 21f4d26b50 | |||
| 627ea268c9 | |||
| 65f77db4d6 | |||
| 03b4f1eb8d | |||
| 82d1fc0a8b | |||
| 3b16de5df4 | |||
| 94d4070653 | |||
| d856a1cd8e | |||
| 7c1d0f7bee | |||
| d02036321f | |||
| 1edadcc6bb | |||
| aa92091d2d | |||
| 14e0385575 | |||
| 6cf861d44e | |||
| 1375240bfa | |||
| 8f391d7d52 | |||
| 84654e81af | |||
| 7ff0524bb7 | |||
| 06c3c92fd6 | |||
| 4c2941e9b6 | |||
| ed7a25646e | |||
| 8c821176cf | |||
| 98ded98583 | |||
| 10ae69e41f | |||
| 04531b4305 | |||
| 7ab4b2481c | |||
| 6fa88e6869 | |||
| 5b90c31645 | |||
| 5eba11faee | |||
| 54b4fc473c | |||
| fe8595cdc9 | |||
| 2565a89474 | |||
| 92b970377d | |||
| dd3a550ec3 | |||
| 7d5d9e2d82 | |||
| 9daa71befc | |||
| f10a2d28df | |||
| d52c6c70bb | |||
| 6e2f46a0da | |||
| 2d6f91d0cd | |||
| f409079d93 | |||
| 7004259867 | |||
| f3128cb6d2 | |||
| 1dfd4d8d48 | |||
| 2f30eb6cbe | |||
| cf45c04069 | |||
| 05d149bcfa | |||
| 3acbc3291c | |||
| 24aa2fd5fa | |||
| cc7d7a56f5 | |||
| da17646b8d | |||
| f9233a1e31 | |||
| c94f536e5a | |||
| b712e7f184 | |||
| b8ab802de5 | |||
| a74eb2989c | |||
| 3a0fc6eeac | |||
| 0f0684e701 | |||
| e9b5bfb572 | |||
| b456b59c44 | |||
| 59ad91fdf8 | |||
| e18d304313 | |||
| 5de5281b49 | |||
| fe0df2a4ef | |||
| deec7894dd | |||
| c9a0c0400e | |||
| 419e62f69b | |||
| 9ffbe3a7a2 | |||
| c8e548e3be | |||
| b9160bd333 | |||
| 8ad8b4cdea | |||
| c51c40a20e | |||
| 2ccb1a8e0a | |||
| 008bb75c04 | |||
| a51fdb8435 | |||
| cdd5d27908 | |||
| 82d3c934c8 | |||
| f2f0270936 | |||
| 00f646d489 | |||
| 90a02df45c | |||
| ad40a15a77 | |||
| d6809cb504 | |||
| ff8d3055b4 | |||
| 1f81449e3c | |||
| c59e0cb046 | |||
| 2d1675c181 | |||
| 48ebca6236 | |||
| bce8e8b807 | |||
| 1f4dfa7603 | |||
| c23af0063d | |||
| 9f6250c489 | |||
| cf6c6f1c17 | |||
| c6163b6ba2 | |||
| 7121a680b4 | |||
| cf391031f0 | |||
| 9cac5305cd | |||
| b0adb8cd3d | |||
| 3d2a1d5d19 | |||
| 3c93c41de1 | |||
| 4baed5d8ab | |||
| f17b02b1fe | |||
| 3c72d07a88 | |||
| 7495c0dfcf | |||
| b0c877d4ae | |||
| e1fd0f8014 | |||
| cb2198b661 | |||
| 68beeeb599 | |||
| 218ce148d5 | |||
| d0cafb06ee | |||
| 5671a58704 | |||
| 9cf74ca405 | |||
| 1c6e3648eb | |||
| b3a4628a79 | |||
| 941cc37c87 | |||
| 906d7ac3d5 | |||
| 588b18d10e | |||
| 90024c5ec7 | |||
| 889005de6c | |||
| 843d3df8bc | |||
| 825b9044b9 | |||
| 92a1be83b6 | |||
| 36e35eb208 | |||
| 2fcdb47fe5 | |||
| 012e4b670e | |||
| 492cc244d8 | |||
| d17d6b43c0 | |||
| 1bf10cd6d6 | |||
| b0cd9140be | |||
| cc6196969f | |||
| 802a7eb1be | |||
| a666f86be0 | |||
| 44fad76d45 | |||
| c8d23a534e | |||
| cf3650c8cd | |||
| 1a8349f821 | |||
| 855771a6b6 | |||
| b711168e44 | |||
| 4134919dde | |||
| 158153e366 | |||
| 6b890d162a | |||
| 1525807d06 | |||
| a356bfd601 | |||
| e6d2a6d5ff | |||
| c8a004422f | |||
| df457fc7bf | |||
| 6e03f1f077 | |||
| 4c13350c14 | |||
| 63a2495c68 | |||
| f9de73e18a | |||
| e6e2be61f0 | |||
| 060f7aa42c | |||
| 8b0ae178b8 | |||
| 6b94e9b78b | |||
| b171bf3127 | |||
| 0c6a179cec | |||
| aa50785c12 | |||
| 252403b1e3 | |||
| bc1d0de057 | |||
| 4a0b9fd0de | |||
| e8777e1857 | |||
| 94a2a16330 | |||
| 004d135d93 | |||
| 0973dcee8a | |||
| c908f7dcd0 | |||
| b7fd889fcd | |||
| 35600c253d | |||
| 673465af77 | |||
| cc5d0dc4fe | |||
| e6da977c48 | |||
| e4aa016713 | |||
| 8b67499a64 | |||
| bcdc7c6811 | |||
| 8f07a79a49 | |||
| cdcf16196c | |||
| a0ed6a379b | |||
| 7ffb44f604 | |||
| 225d0388ce | |||
| 0468db8cc2 | |||
| 695f612110 | |||
| 9c07852b87 | |||
| 9ef198fb86 | |||
| fd34e31462 | |||
| 8eb49427fd | |||
| ebafb3c3dd | |||
| 09c6154949 | |||
| 53c8661cd3 | |||
| 3d5eec897e | |||
| 3763ec3737 | |||
| f1216795d2 | |||
| 0ac70df00f | |||
| 7d0a985f1d | |||
| c5ff67b6e0 | |||
| db6b9ac176 | |||
| 77475af3c6 | |||
| 805133d10c | |||
| fff8fb72c5 | |||
| e02b18cea3 | |||
| b8e9307ce2 | |||
| 00b22eb902 | |||
| be954143c3 | |||
| b314642586 | |||
| b5fc315b37 | |||
| 26ff9f63bb | |||
| 7e5fb4d42f | |||
| 762bb786be | |||
| cec87765fc | |||
| b966fae926 | |||
| f1ddf4e57e | |||
| 64e65dc7d3 | |||
| 1e07a88aed | |||
| 6e49cc65a9 | |||
| 06d579118a | |||
| f0e4b5b930 | |||
| 088127a9a5 | |||
| a4b4d077fa | |||
| bc2ec4d0b7 | |||
| e4de101553 | |||
| 6207416b32 | |||
| f0202241f4 | |||
| 0e8c94af1e | |||
| 922ee616ec | |||
| 98c235bc21 | |||
| 70f1f9ffac | |||
| 409f81f0d8 | |||
| 1aa90bef37 | |||
| c5ea9494df | |||
| 9a34db7008 | |||
| 988b7fca0f | |||
| bdc6489db4 | |||
| 49b096b601 | |||
| 31d83f6a26 | |||
| 03e52b7a72 | |||
| 551e950c39 | |||
| a7620f83a6 | |||
| 0384181ee1 | |||
| fd59f42819 | |||
| cc51774259 | |||
| 5a19245ee0 | |||
| 45f45b7547 | |||
| 530bfaf3b3 | |||
| 6ea70ab9cf | |||
| a0e2d68dae | |||
| 1447e8bfb5 | |||
| c0480a50ea | |||
| 09ba2be416 | |||
| 115ce3305d | |||
| 960b2022ed | |||
| 252b442ca9 | |||
| 03b9ddda9c | |||
| 968d7d7fda | |||
| b211f1cd12 | |||
| 870d4349f4 | |||
| d88bc59720 | |||
| 7c00775515 | |||
| 4b5536b282 | |||
| fb84cfee1c | |||
| 9132262106 | |||
| 5de9e708e9 | |||
| 4df63c7287 | |||
| a88ca031d0 | |||
| 452706f35c | |||
| 8b0aa194cf | |||
| b9aa7e9206 | |||
| e35f106482 | |||
| b7a97cf8ee | |||
| 85a28999fb | |||
| 9208115981 | |||
| afbdaecd0a | |||
| 558f3d649d | |||
| 417511d577 | |||
| df350534ce | |||
| 7167235146 | |||
| 7a7f2eb7de | |||
| cd28a53f6a | |||
| 1fe31ac0ec | |||
| ffce8e3ba5 | |||
| a28a417ebc | |||
| 6cdb56d13f | |||
| 0516e587b4 | |||
| 5b4063105b | |||
| d9a5548762 | |||
| cae6cffb7b | |||
| ede782abed | |||
| 00afbad7ac | |||
| edd09ef585 | |||
| 1e09066779 | |||
| 32edb48e16 | |||
| b116eef147 | |||
| 74d1cd2d01 | |||
| fd48d82e42 | |||
| 480d54c2d8 | |||
| ab9c39b862 | |||
| dd122d1bac | |||
| f9f5e8f925 | |||
| aa2f4a091c | |||
| 13fc699d4a | |||
| f5e8d473ad | |||
| 35353d75f5 | |||
| 0f103c3c00 | |||
| 019428a241 | |||
| 6200edea25 | |||
| fc7395df96 | |||
| 272f662a34 | |||
| d59d9cde95 | |||
| c71fbcf436 | |||
| 54c79445db | |||
| a82ba26f86 | |||
| 28af87391d | |||
| 0aabd33538 | |||
| bd9076c4ff | |||
| 1aed761893 | |||
| a2a6f6dcab | |||
| db5dd0f6ee | |||
| 42c3dcf258 | |||
| 51a115d65a | |||
| fc0fbd6a37 | |||
| d80d76724c | |||
| 77da0a6d30 | |||
| 79b251d8b9 | |||
| 4f19f6a08c | |||
| d2a6eab1e6 | |||
| c7cf051502 | |||
| 6e21503e6b | |||
| 78d71a4e7e | |||
| b8f32d46cf | |||
| 443782decf | |||
| 54e79e5eb8 | |||
| 221cd4ffd2 | |||
| 354b2fb3cb | |||
| 7cb2c36ac9 | |||
| 11d73595fc | |||
| c241a6e280 | |||
| 198ff143f6 | |||
| 7b8b797f9c | |||
| a41b25eea7 | |||
| 06eac68f9d | |||
| 8abf84ebc0 | |||
| e9aa1f6dd6 | |||
| 8e7b838bf7 | |||
| 32db3c3c9b | |||
| 502586e5d5 | |||
| d92d599d8a | |||
| 3487056c7d | |||
| dbcfb08c15 | |||
| e96141090e | |||
| 5bd25fc2ed | |||
| 334932b298 | |||
| 661006d7fb | |||
| c917249517 | |||
| d0ca732fe7 | |||
| 52821cd1df | |||
| 328e0b8ff7 | |||
| f0147cc89a | |||
| 78a7f152e7 | |||
| e936dd16bf | |||
| 3d4579288c | |||
| b60df8b074 | |||
| c4b92fa0a9 | |||
| 9808d44c71 | |||
| 1bdd135386 | |||
| 740a75f2e6 | |||
| b62533fa3e | |||
| 044349e65f | |||
| e8b0bd4d0a | |||
| 45bdf53a41 | |||
| 00e94212bd | |||
| 1805aee7ba | |||
| 8ba2123a26 | |||
| 687c08527d | |||
| f0744f024d | |||
| 9e82599464 | |||
| 7a60df370e | |||
| aee4a041fe | |||
| 6fa06e5483 | |||
| 15962481ee | |||
| 6bf695cd4b | |||
| 7ecbc14c39 | |||
| fb0e531f4a | |||
| bd44e6d071 | |||
| 3ae37f1c46 | |||
| 8c7a1f7075 | |||
| 3e97d8a5f1 | |||
| 6138257a89 | |||
| 0bd62985b9 | |||
| a346d757fd | |||
| 84f3970730 | |||
| 050e22918a | |||
| f205776695 | |||
| 4d427bcbc3 | |||
| 0b4910e3c3 | |||
| 11a59debdf | |||
| dbe2c2637e | |||
| d6ce13c356 | |||
| 6d094a8b3e | |||
| 4d0456fd68 | |||
| ad83f940a7 | |||
| edc7e02f58 | |||
| 7dfc5029a3 | |||
| 8e02722b52 | |||
| 984581e01a | |||
| 1de7ba7e86 | |||
| a3fa86f7ee | |||
| a589f89c4c | |||
| 3b599aed4c | |||
| 6953db74c6 | |||
| 1ad473a539 | |||
| fafda9ec92 | |||
| 11b85bf565 | |||
| 6ecc6670dc | |||
| 553f0fe23e | |||
| 77ddf67201 | |||
| a092bc2689 | |||
| 38e0786463 | |||
| 180b17d315 | |||
| 8acbd784a0 | |||
| 07efe1e1b8 | |||
| 7ea53cc3a1 | |||
| de21eea21b | |||
| 8f8ed6dff3 | |||
| 8915bbfeb4 | |||
| df3a42acce | |||
| 2a7ef82d23 | |||
| 95168f22e3 | |||
| 4360753263 | |||
| ad491cb465 | |||
| e5bed80c96 | |||
| 9bf3046d4c | |||
| da8dc0f7a6 | |||
| b66a4afd28 | |||
| a94c53534c | |||
| 6b22c9130f | |||
| 7d5927bbc8 | |||
| 240d02a124 | |||
| 92ef1c4cbb | |||
| 5ce57d0803 | |||
| 1c134d757c | |||
| 32d808b230 | |||
| ee3def492a | |||
| 41bd91fcd2 | |||
| 02c8542848 | |||
| 844abd38ce | |||
| d9b6f28bb5 | |||
| ed85b838ec | |||
| f82b138a3d | |||
| 89c38991b9 | |||
| 0e4c06cc3e | |||
| 7e510a2582 | |||
| f3cb9c4fdd | |||
| f0f6130272 | |||
| 0ebb299fd0 | |||
| 9d96525f12 | |||
| fc6f9251f7 | |||
| 53cb317155 | |||
| 4215b5808f | |||
| e64ca3eb93 | |||
| 094c4747d3 | |||
| 62f0843283 | |||
| 8062e304b7 | |||
| 18e7ead78a | |||
| 3cab1dc49f | |||
| f8dcbb8fb7 | |||
| c82f38c103 | |||
| fe0e7af033 | |||
| ac51902435 | |||
| e0ff342702 | |||
| cb4f5ea963 | |||
| a822430e1d | |||
| dc40ce7d9e | |||
| 383a119127 | |||
| 3f492e6208 | |||
| 0cec0cf595 | |||
| bc19aea438 | |||
| 211850c813 | |||
| c1312fb288 | |||
| 0c2934c489 | |||
| ad34147f28 | |||
| 1eb6c28617 | |||
| 2e8a188bce | |||
| a773958403 | |||
| 942b2240a7 |
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["env", "react"],
|
|
||||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"]
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
# Unix-style newlines with a newline ending every file
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
# Matches multiple files with brace expansion notation
|
|
||||||
# Set default charset
|
|
||||||
[*.{js,jsx,html,sass}]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directory
|
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# Ignore build files
|
|
||||||
public
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
addons:
|
|
||||||
firefox: latest
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: linux
|
|
||||||
node_js: "6"
|
|
||||||
- os: linux
|
|
||||||
env: CXX=g++-4.8
|
|
||||||
node_js: "7"
|
|
||||||
- os: linux
|
|
||||||
node_js: "8"
|
|
||||||
- os: linux
|
|
||||||
env: CXX=g++-4.8
|
|
||||||
node_js: "9"
|
|
||||||
- os: osx
|
|
||||||
node_js: "6"
|
|
||||||
- os: osx
|
|
||||||
node_js: "7"
|
|
||||||
- os: osx
|
|
||||||
node_js: "8"
|
|
||||||
- os: osx
|
|
||||||
node_js: "9"
|
|
||||||
before_install:
|
|
||||||
- export CHROME_BIN=chromium-browser
|
|
||||||
- export DISPLAY=:99.0
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi
|
|
||||||
install:
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- mkdir public
|
|
||||||
- node --stack_size=100000 $(which npm) run build
|
|
||||||
- npm run lint
|
|
||||||
- npm run lint-styles
|
|
||||||
- npm run test
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- ubuntu-toolchain-r-test
|
|
||||||
packages:
|
|
||||||
- g++-4.8
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
FROM nodesource/xenial:6.1.0
|
|
||||||
|
|
||||||
EXPOSE 8888
|
|
||||||
|
|
||||||
ENV HOME /maputnik
|
|
||||||
RUN mkdir ${HOME}
|
|
||||||
|
|
||||||
COPY . ${HOME}/
|
|
||||||
|
|
||||||
WORKDIR ${HOME}
|
|
||||||
|
|
||||||
RUN npm install -d --dev
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
CMD npm run start -- --host 0.0.0.0
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015 Lukas Martinelli
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# Maputnik
|
|
||||||
|
|
||||||
[][travis]
|
|
||||||
[][appveyor]
|
|
||||||
[][dm-prod]
|
|
||||||
[][dm-dev]
|
|
||||||
[][license]
|
|
||||||
|
|
||||||
[travis]: https://travis-ci.org/maputnik/editor
|
|
||||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
|
||||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
|
||||||
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
|
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
|
||||||
|
|
||||||
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
|
||||||
|
|
||||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
|
||||||
targeted at developers and map designers.
|
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
|
||||||
|
|
||||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
|
||||||
|
|
||||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
|
||||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
|
||||||
|
|
||||||
[](https://youtu.be/XoDh0gEnBQo)
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
|
||||||
|
|
||||||
We ensure building and developing Maputnik works with
|
|
||||||
|
|
||||||
- Linux, OSX and Windows
|
|
||||||
- Node >4
|
|
||||||
|
|
||||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# install dependencies
|
|
||||||
npm install
|
|
||||||
# start dev server
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs
|
|
||||||
|
|
||||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this.
|
|
||||||
Snippet from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions->
|
|
||||||
|
|
||||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Lint the JavaScript code.
|
|
||||||
|
|
||||||
```
|
|
||||||
# run linter
|
|
||||||
npm run lint
|
|
||||||
npm run lint-styles
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related Projects
|
|
||||||
|
|
||||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
|
||||||
|
|
||||||
### Gold
|
|
||||||
|
|
||||||
- [Wemap](https://getwemap.com/)
|
|
||||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
|
||||||
- [Terranodo](http://terranodo.io/)
|
|
||||||
|
|
||||||
<a href="https://getwemap.com/">
|
|
||||||
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" />
|
|
||||||
</a>
|
|
||||||
<a href="http://terranodo.io/">
|
|
||||||
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.orbiconinformatik.dk/">
|
|
||||||
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Silver
|
|
||||||
|
|
||||||
- [Klokan Technologies](https://www.klokantech.com/)
|
|
||||||
- [Geofabrik](http://www.geofabrik.de/)
|
|
||||||
- [Dreipol](https://www.dreipol.ch/)
|
|
||||||
|
|
||||||
<a href="https://www.klokantech.com/">
|
|
||||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
|
|
||||||
</a>
|
|
||||||
<a href="http://www.geofabrik.de/">
|
|
||||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.dreipol.ch/">
|
|
||||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Individuals
|
|
||||||
|
|
||||||
**Influential Stakeholder**
|
|
||||||
|
|
||||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
|
||||||
|
|
||||||
**Stakeholder**
|
|
||||||
|
|
||||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
|
||||||
|
|
||||||
**Supporter**
|
|
||||||
|
|
||||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
|
||||||
|
|
||||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is a independent style editor for the
|
|
||||||
open source technology in the Mapbox GL ecosystem.
|
|
||||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- nodejs_version: "6"
|
|
||||||
- nodejs_version: "7"
|
|
||||||
- nodejs_version: "8"
|
|
||||||
- nodejs_version: "9"
|
|
||||||
platform:
|
|
||||||
- x86
|
|
||||||
- x64
|
|
||||||
install:
|
|
||||||
- ps: Install-Product node $env:nodejs_version
|
|
||||||
- md public
|
|
||||||
- npm install --global --production windows-build-tools
|
|
||||||
- npm install
|
|
||||||
build_script:
|
|
||||||
- npm run build
|
|
||||||
test_script:
|
|
||||||
- npm run lint
|
|
||||||
- npm test
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,6 +0,0 @@
|
|||||||
machine:
|
|
||||||
node:
|
|
||||||
version: 6
|
|
||||||
test:
|
|
||||||
post:
|
|
||||||
- npm run build
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
var webpack = require("webpack");
|
|
||||||
var WebpackDevServer = require("webpack-dev-server");
|
|
||||||
var webpackConfig = require("./webpack.production.config");
|
|
||||||
var testConfig = require("../test/config/specs");
|
|
||||||
|
|
||||||
|
|
||||||
var server;
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
specs: [
|
|
||||||
'./test/specs/**/*.js'
|
|
||||||
],
|
|
||||||
exclude: [
|
|
||||||
],
|
|
||||||
maxInstances: 10,
|
|
||||||
capabilities: [{
|
|
||||||
maxInstances: 5,
|
|
||||||
browserName: 'firefox'
|
|
||||||
}],
|
|
||||||
sync: true,
|
|
||||||
logLevel: 'verbose',
|
|
||||||
coloredLogs: true,
|
|
||||||
bail: 0,
|
|
||||||
screenshotPath: './errorShots/',
|
|
||||||
baseUrl: 'http://localhost',
|
|
||||||
waitforTimeout: 10000,
|
|
||||||
connectionRetryTimeout: 90000,
|
|
||||||
connectionRetryCount: 3,
|
|
||||||
services: ['phantomjs'],
|
|
||||||
framework: 'mocha',
|
|
||||||
reporters: ['spec'],
|
|
||||||
phantomjsOpts: {
|
|
||||||
webdriverLogfile: 'phantomjs.log'
|
|
||||||
},
|
|
||||||
mochaOpts: {
|
|
||||||
ui: 'bdd',
|
|
||||||
// Because we don't know how long the initial build will take...
|
|
||||||
timeout: 2*60*1000
|
|
||||||
},
|
|
||||||
onPrepare: function (config, capabilities) {
|
|
||||||
var compiler = webpack(webpackConfig);
|
|
||||||
server = new WebpackDevServer(compiler, {});
|
|
||||||
server.listen(testConfig.port);
|
|
||||||
},
|
|
||||||
onComplete: function(exitCode) {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var loaders = require('./webpack.loaders');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
|
||||||
const PORT = process.env.PORT || "8888";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: 'web',
|
|
||||||
entry: [
|
|
||||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
|
||||||
`webpack/hot/only-dev-server`,
|
|
||||||
`./src/index.jsx` // Your appʼs entry point
|
|
||||||
],
|
|
||||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, '..', 'public'),
|
|
||||||
filename: 'bundle.js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/,
|
|
||||||
/openlayers\/dist\/ol.js/
|
|
||||||
],
|
|
||||||
loaders: loaders
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
contentBase: "./public",
|
|
||||||
// do not print bundle build stats
|
|
||||||
noInfo: true,
|
|
||||||
// enable HMR
|
|
||||||
hot: true,
|
|
||||||
// embed the webpack-dev-server runtime into the bundle
|
|
||||||
inline: true,
|
|
||||||
// serve index.html in place of 404 responses to allow HTML5 history
|
|
||||||
historyApiFallback: true,
|
|
||||||
port: PORT,
|
|
||||||
host: HOST,
|
|
||||||
watchOptions: {
|
|
||||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
|
||||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
|
||||||
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
|
||||||
watch: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: 'Maputnik',
|
|
||||||
template: './src/template.html'
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
module.exports = [
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: /(node_modules|bower_components|public)/,
|
|
||||||
loaders: ['react-hot-loader/webpack']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: /(.*node_modules(?![\/\\]@mapbox[\/\\]mapbox-gl-style-spec)|bower_components|public)/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
query: {
|
|
||||||
presets: ['env', 'react'],
|
|
||||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(eot|ttf|woff|woff2)$/,
|
|
||||||
loader: 'file-loader?name=fonts/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.ico$/,
|
|
||||||
loader: 'file-loader?name=[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(svg|gif|jpg|png)$/,
|
|
||||||
loader: 'file-loader?name=img/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.json$/,
|
|
||||||
loader: 'json-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
|
||||||
loaders: ["style-loader", "css-loader", "sass-loader"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
|
||||||
loaders: [
|
|
||||||
'style-loader?sourceMap',
|
|
||||||
'css-loader'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
|
|
||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var loaders = require('./webpack.loaders');
|
|
||||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
|
||||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
var OUTPATH;
|
|
||||||
if(process.env.CIRCLE_ARTIFACTS) {
|
|
||||||
OUTPATH = path.join(process.env.CIRCLE_ARTIFACTS, "build");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
OUTPATH = path.join(__dirname, '..', 'public');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
app: './src/index.jsx',
|
|
||||||
vendor: [
|
|
||||||
'file-saver',
|
|
||||||
'mapbox-gl/dist/mapbox-gl.js',
|
|
||||||
"lodash.clonedeep",
|
|
||||||
"lodash.throttle",
|
|
||||||
'color',
|
|
||||||
'react',
|
|
||||||
"react-dom",
|
|
||||||
"react-color",
|
|
||||||
"react-file-reader-input",
|
|
||||||
"react-collapse",
|
|
||||||
"react-height",
|
|
||||||
"react-icon-base",
|
|
||||||
"react-motion",
|
|
||||||
"react-sortable-hoc",
|
|
||||||
"request",
|
|
||||||
//TODO: Icons raise multi vendor errors?
|
|
||||||
//"react-icons",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: OUTPATH,
|
|
||||||
filename: '[name].[chunkhash].js',
|
|
||||||
chunkFilename: '[chunkhash].js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/,
|
|
||||||
/openlayers\/dist\/ol.js/
|
|
||||||
],
|
|
||||||
loaders
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
|
|
||||||
new WebpackCleanupPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: '"production"'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
|
||||||
compress: {
|
|
||||||
warnings: false,
|
|
||||||
screw_ie8: true,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new ExtractTextPlugin('[contenthash].css', {
|
|
||||||
allChunks: true
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './src/template.html',
|
|
||||||
title: 'Maputnik'
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: 'static',
|
|
||||||
defaultSizes: 'gzip',
|
|
||||||
openAnalyzer: false,
|
|
||||||
generateStatsFile: true,
|
|
||||||
reportFilename: 'bundle-stats.html',
|
|
||||||
statsFilename: 'bundle-stats.json',
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Maputnik</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="manifest" href="/maputnik/assets/manifest-BrZzkYP9.json">
|
||||||
|
<link rel="icon" href="/maputnik/assets/favicon-DBn6BKLx.ico" type="image/x-icon" />
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading__logo img {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading__text {
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script type="module" crossorigin src="/maputnik/assets/index-BYSOK7KO.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/maputnik/assets/index-vOMA9Lgt.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1">
|
||||||
|
<defs>
|
||||||
|
<filter id="protanopia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.567, 0.433, 0, 0, 0
|
||||||
|
0.558, 0.442, 0, 0, 0
|
||||||
|
0, 0.242, 0.758, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="protanomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.817, 0.183, 0, 0, 0
|
||||||
|
0.333, 0.667, 0, 0, 0
|
||||||
|
0, 0.125, 0.875, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="deuteranopia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.625, 0.375, 0, 0, 0
|
||||||
|
0.7, 0.3, 0, 0, 0
|
||||||
|
0, 0.3, 0.7, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="deuteranomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.8, 0.2, 0, 0, 0
|
||||||
|
0.258, 0.742, 0, 0, 0
|
||||||
|
0, 0.142, 0.858, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="tritanopia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.95, 0.05, 0, 0, 0
|
||||||
|
0, 0.433, 0.567, 0, 0
|
||||||
|
0, 0.475, 0.525, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="tritanomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.967, 0.033, 0, 0, 0
|
||||||
|
0, 0.733, 0.267, 0, 0
|
||||||
|
0, 0.183, 0.817, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="achromatopsia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.299, 0.587, 0.114, 0, 0
|
||||||
|
0.299, 0.587, 0.114, 0, 0
|
||||||
|
0.299, 0.587, 0.114, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="achromatomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.618, 0.320, 0.062, 0, 0
|
||||||
|
0.163, 0.775, 0.062, 0, 0
|
||||||
|
0.163, 0.320, 0.516, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
<div class="loading">
|
||||||
|
<div class="loading__logo">
|
||||||
|
<img inline src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20width='1200'%20height='1200'%20viewBox='0%200%20100%20100'%3e%3cstyle%3e@keyframes%20circle-anim{0%25,40%25{fill-opacity:0}60%25,to{fill-opacity:1}}.circle0,.circle1,.circle2,.circle3,.circle4,.circle5{stroke-opacity:0;animation-name:circle-anim;will-change:transform;animation-timing-function:east-in-out;animation-duration:800ms;animation-iteration-count:infinite;animation-direction:alternate}.circle0{animation-delay:100ms}.circle1{animation-delay:200ms}.circle2{animation-delay:300ms}.circle3{animation-delay:400ms}.circle4{animation-delay:500ms}.circle5{animation-delay:600ms}%3c/style%3e%3cg%20class='map'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--map__main'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line1'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line2'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line3'%20fill='none'%3e%3c/use%3e%3c/g%3e%3cg%20class='palette'%3e%3cuse%20xlink:href='%23ref-1--palette__main'%20fill='%23fff'%20stroke='%23000'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--palette__inner'%20fill='none'%20stroke='%23000'%3e%3c/use%3e%3cuse%20class='circle5'%20xlink:href='%23ref-1--palette__circle5'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle4'%20xlink:href='%23ref-1--palette__circle4'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle3'%20xlink:href='%23ref-1--palette__circle3'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle2'%20xlink:href='%23ref-1--palette__circle2'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle1'%20xlink:href='%23ref-1--palette__circle1'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle0'%20xlink:href='%23ref-1--palette__circle0'%20fill='%234eba6f'%3e%3c/use%3e%3c/g%3e%3cg%20class='brush'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--brush__bottom'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--brush__top'%20fill='%23fff'%3e%3c/use%3e%3c/g%3e%3cdefs%3e%3cpath%20id='ref-1--map__main'%20stroke-width='2.366'%20stroke-linejoin='round'%20d='M18.84%207.717l15.44%207.542%2015.75-7.762%2015.7%207.857L81.005%207.67%2096.31%2054.052%2073.598%2062.12%2050.93%2053.872l-25.1%208.066-22.668-8.066z'%3e%3c/path%3e%3cpath%20id='ref-1--map__line1'%20d='M65.556%2015.07l7.647%2046.838'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line2'%20d='M50.261%207.422l.717%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line3'%20d='M34.011%2015.07l-8.603%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--palette__main'%20stroke-width='2.3'%20d='M47.352%2030.887c7.993.226%2016.934%209.725%2017.954%2015.25%201.02%205.527-.743%2011.125-4.298%2013.875-3.554%202.75-8.6%202.905-8.723%208.302-.097%204.237%208.457%208.5%208.088%2015.653-.406%207.857-15.508%2013.15-30.943%206.102-8.556-3.906-14.249-13.653-13.385-26.238C16.833%2052.334%2022.32%2043.658%2027.382%2039c5.977-5.503%2011.977-8.337%2019.97-8.112z'%3e%3c/path%3e%3ccircle%20id='ref-1--palette__inner'%20stroke-width='2.3'%20cx='41.873'%20cy='61.901'%20r='6.389'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle5'%20cy='44.56'%20cx='54.347'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle4'%20cx='40.443'%20cy='41.555'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle3'%20r='4.336'%20cy='51.102'%20cx='29.651'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle2'%20cx='25.293'%20cy='65.836'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle1'%20r='4.336'%20cy='79.326'%20cx='32.764'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle0'%20cx='46.669'%20cy='80.571'%20r='4.336'%3e%3c/circle%3e%3cpath%20id='ref-1--brush__bottom'%20d='M76.333%2089.333c-1.645-9.794-4.375-35.26-4.32-37.887.056-2.627%202.52-4.34%205.36-4.317%202.842.022%205.098%201.87%205.314%204.27.107%201.2-1.576%2028.06-2.318%2037.844-.332%204.374-3.31%204.413-4.036.09z'%20stroke-width='2.3'%20stroke-linejoin='round'%3e%3c/path%3e%3cpath%20id='ref-1--brush__top'%20stroke-linejoin='round'%20stroke-width='2.3'%20d='M77.184%2026.428s-5.621%207.02-5.621%2011.978c0%204.957%202.206%206.878%205.81%206.878%203.606%200%205.148-1.708%205.29-6.736.142-5.028-5.479-12.12-5.479-12.12z'%3e%3c/path%3e%3c/defs%3e%3c/svg%3e" />
|
||||||
|
</div>
|
||||||
|
<div class="loading__text">Loading…</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,133 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "maputnik",
|
|
||||||
"version": "1.1.0-beta3",
|
|
||||||
"description": "A MapboxGL visual style editor",
|
|
||||||
"main": "''",
|
|
||||||
"scripts": {
|
|
||||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
|
||||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
|
||||||
"test": "wdio config/wdio.conf.js",
|
|
||||||
"test-watch": "wdio config/wdio.conf.js --watch",
|
|
||||||
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
|
||||||
"lint": "eslint --ext js --ext jsx {src,test}",
|
|
||||||
"lint-styles": "stylelint 'src/styles/*.scss'"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/maputnik/editor"
|
|
||||||
},
|
|
||||||
"author": "Lukas Martinelli",
|
|
||||||
"license": "MIT",
|
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.1",
|
|
||||||
"@mapbox/mapbox-gl-style-spec": "^10.0.1",
|
|
||||||
"classnames": "^2.2.5",
|
|
||||||
"codemirror": "^5.32.0",
|
|
||||||
"color": "^2.0.0",
|
|
||||||
"file-saver": "^1.3.3",
|
|
||||||
"github-api": "^3.0.0",
|
|
||||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
|
||||||
"lodash.capitalize": "^4.2.1",
|
|
||||||
"lodash.clonedeep": "^4.5.0",
|
|
||||||
"lodash.isequal": "^4.5.0",
|
|
||||||
"lodash.throttle": "^4.1.1",
|
|
||||||
"mapbox-gl": "^0.43.0",
|
|
||||||
"mapbox-gl-inspect": "^1.2.6",
|
|
||||||
"maputnik-design": "github:maputnik/design",
|
|
||||||
"mousetrap": "^1.6.1",
|
|
||||||
"ol-mapbox-style": "^1.0.1",
|
|
||||||
"openlayers": "^4.4.2",
|
|
||||||
"prop-types": "^15.6.0",
|
|
||||||
"react": "16.0.0",
|
|
||||||
"react-addons-pure-render-mixin": "^15.6.2",
|
|
||||||
"react-autocomplete": "^1.7.2",
|
|
||||||
"react-codemirror2": "^3.0.7",
|
|
||||||
"react-collapse": "^4.0.3",
|
|
||||||
"react-color": "^2.13.8",
|
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
|
||||||
"react-dom": "16.0.0",
|
|
||||||
"react-file-reader-input": "^1.1.4",
|
|
||||||
"react-height": "^3.0.0",
|
|
||||||
"react-icon-base": "^2.1.1",
|
|
||||||
"react-icons": "^2.2.7",
|
|
||||||
"react-motion": "^0.5.2",
|
|
||||||
"react-sortable-hoc": "^0.6.8",
|
|
||||||
"reconnecting-websocket": "^3.2.2",
|
|
||||||
"request": "^2.83.0",
|
|
||||||
"url": "^0.11.0"
|
|
||||||
},
|
|
||||||
"jshintConfig": {
|
|
||||||
"esversion": 6
|
|
||||||
},
|
|
||||||
"stylelint": {
|
|
||||||
"extends": "stylelint-config-standard"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"impliedStrict": true,
|
|
||||||
"experimentalObjectRestSpread": true,
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-core": "^6.26.0",
|
|
||||||
"babel-eslint": "^8.0.2",
|
|
||||||
"babel-loader": "7.1.1",
|
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
|
||||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
|
||||||
"babel-preset-env": "^1.6.1",
|
|
||||||
"babel-preset-react": "^6.24.1",
|
|
||||||
"babel-runtime": "^6.26.0",
|
|
||||||
"base64-loader": "^1.0.0",
|
|
||||||
"copy-webpack-plugin": "^4.2.0",
|
|
||||||
"css-loader": "^0.28.7",
|
|
||||||
"eslint": "^4.10.0",
|
|
||||||
"eslint-plugin-react": "^7.4.0",
|
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
|
||||||
"file-loader": "^1.1.5",
|
|
||||||
"html-webpack-plugin": "^2.30.1",
|
|
||||||
"json-loader": "^0.5.7",
|
|
||||||
"karma": "^1.7.1",
|
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
|
||||||
"karma-firefox-launcher": "^1.0.1",
|
|
||||||
"karma-mocha": "^1.3.0",
|
|
||||||
"karma-webpack": "^2.0.5",
|
|
||||||
"mocha": "^4.0.1",
|
|
||||||
"mocha-loader": "^1.1.1",
|
|
||||||
"node-sass": "^4.6.0",
|
|
||||||
"react-hot-loader": "^3.1.1",
|
|
||||||
"sass-loader": "^6.0.6",
|
|
||||||
"style-loader": "^0.19.0",
|
|
||||||
"stylelint": "^7.13.0",
|
|
||||||
"stylelint-config-standard": "^15.0.1",
|
|
||||||
"transform-loader": "^0.2.4",
|
|
||||||
"wdio-mocha-framework": "^0.5.11",
|
|
||||||
"wdio-phantomjs-service": "^0.2.2",
|
|
||||||
"wdio-spec-reporter": "^0.1.2",
|
|
||||||
"webdriverio": "^4.8.0",
|
|
||||||
"webpack": "^3.8.1",
|
|
||||||
"webpack-bundle-analyzer": "^2.9.0",
|
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
|
||||||
"webpack-dev-server": "^2.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
.cm-s-maputnik.CodeMirror {
|
|
||||||
height: 100%;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
|
||||||
color: #8e8e8e;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-gutters {
|
|
||||||
background: #212328;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-cursor {
|
|
||||||
border-left: solid thin #8e8e8e !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
|
|
||||||
background: rgba(255, 255, 255, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-line::selection,
|
|
||||||
.cm-s-maputnik .CodeMirror-line > span::selection,
|
|
||||||
.cm-s-maputnik .CodeMirror-line > span > span::selection {
|
|
||||||
background: rgba(255, 255, 255, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-line::-moz-selection,
|
|
||||||
.cm-s-maputnik .CodeMirror-line > span::-moz-selection,
|
|
||||||
.cm-s-maputnik .CodeMirror-line > span > span::-moz-selection {
|
|
||||||
background: rgba(255, 255, 255, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik span.cm-string, .cm-s-maputnik span.cm-string-2 {
|
|
||||||
color: #8f9d6a;
|
|
||||||
}
|
|
||||||
.cm-s-maputnik span.cm-number { color: #91675f; }
|
|
||||||
.cm-s-maputnik span.cm-property { color: #b8a077; }
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-activeline-background {
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-matchingbracket {
|
|
||||||
text-decoration: underline; color: white !important;
|
|
||||||
}
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Mousetrap from 'mousetrap'
|
|
||||||
|
|
||||||
import MapboxGlMap from './map/MapboxGlMap'
|
|
||||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
|
||||||
import LayerList from './layers/LayerList'
|
|
||||||
import LayerEditor from './layers/LayerEditor'
|
|
||||||
import Toolbar from './Toolbar'
|
|
||||||
import AppLayout from './AppLayout'
|
|
||||||
import MessagePanel from './MessagePanel'
|
|
||||||
|
|
||||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import style from '../libs/style.js'
|
|
||||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
|
||||||
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
|
||||||
import { ApiStyleStore } from '../libs/apistore'
|
|
||||||
import { RevisionStore } from '../libs/revisions'
|
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
|
||||||
import tokens from '../config/tokens.json'
|
|
||||||
import isEqual from 'lodash.isequal'
|
|
||||||
|
|
||||||
function updateRootSpec(spec, fieldName, newValues) {
|
|
||||||
return {
|
|
||||||
...spec,
|
|
||||||
$root: {
|
|
||||||
...spec.$root,
|
|
||||||
[fieldName]: {
|
|
||||||
...spec.$root[fieldName],
|
|
||||||
values: newValues
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class App extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.revisionStore = new RevisionStore()
|
|
||||||
this.styleStore = new ApiStyleStore({
|
|
||||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styleUrl = initialStyleUrl()
|
|
||||||
if(styleUrl) {
|
|
||||||
this.styleStore = new StyleStore()
|
|
||||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
|
||||||
} else {
|
|
||||||
this.styleStore.init(err => {
|
|
||||||
if(err) {
|
|
||||||
console.log('Falling back to local storage for storing styles')
|
|
||||||
this.styleStore = new StyleStore()
|
|
||||||
}
|
|
||||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
errors: [],
|
|
||||||
infos: [],
|
|
||||||
mapStyle: style.emptyStyle,
|
|
||||||
selectedLayerIndex: 0,
|
|
||||||
sources: {},
|
|
||||||
vectorLayers: {},
|
|
||||||
inspectModeEnabled: false,
|
|
||||||
spec: styleSpec.latest,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layerWatcher = new LayerWatcher({
|
|
||||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.fetchSources();
|
|
||||||
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this));
|
|
||||||
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
Mousetrap.unbind(['ctrl+z'], this.onUndo.bind(this));
|
|
||||||
Mousetrap.unbind(['ctrl+y'], this.onRedo.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onReset() {
|
|
||||||
this.styleStore.purge()
|
|
||||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
|
||||||
}
|
|
||||||
|
|
||||||
saveStyle(snapshotStyle) {
|
|
||||||
this.styleStore.save(snapshotStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFonts(urlTemplate) {
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
|
||||||
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
|
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIcons(baseUrl) {
|
|
||||||
downloadSpriteMetadata(baseUrl, icons => {
|
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onStyleChanged(newStyle, save=true) {
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
|
||||||
this.updateFonts(newStyle.glyphs)
|
|
||||||
}
|
|
||||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
|
||||||
this.updateIcons(newStyle.sprite)
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = styleSpec.validate(newStyle, styleSpec.latest)
|
|
||||||
if(errors.length === 0) {
|
|
||||||
this.revisionStore.addRevision(newStyle)
|
|
||||||
if(save) this.saveStyle(newStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: newStyle,
|
|
||||||
errors: [],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
errors: errors.map(err => err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchSources();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUndo() {
|
|
||||||
const activeStyle = this.revisionStore.undo()
|
|
||||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
|
||||||
this.saveStyle(activeStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onRedo() {
|
|
||||||
const activeStyle = this.revisionStore.redo()
|
|
||||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
|
||||||
this.saveStyle(activeStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayersChange(changedLayers) {
|
|
||||||
const changedStyle = {
|
|
||||||
...this.state.mapStyle,
|
|
||||||
layers: changedLayers
|
|
||||||
}
|
|
||||||
this.onStyleChanged(changedStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerIdChange(oldId, newId) {
|
|
||||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
|
||||||
|
|
||||||
changedLayers[idx] = {
|
|
||||||
...changedLayers[idx],
|
|
||||||
id: newId
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onLayersChange(changedLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerChanged(layer) {
|
|
||||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(changedLayers, layer.id)
|
|
||||||
changedLayers[idx] = layer
|
|
||||||
|
|
||||||
this.onLayersChange(changedLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeInspectMode() {
|
|
||||||
this.setState({
|
|
||||||
inspectModeEnabled: !this.state.inspectModeEnabled
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchSources() {
|
|
||||||
const sourceList = {...this.state.sources};
|
|
||||||
|
|
||||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
|
||||||
if(sourceList.hasOwnProperty(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceList[key] = {
|
|
||||||
type: val.type,
|
|
||||||
layers: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector") {
|
|
||||||
const url = val.url;
|
|
||||||
fetch(url)
|
|
||||||
.then((response) => {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
// Create new objects before setState
|
|
||||||
const sources = Object.assign({}, this.state.sources);
|
|
||||||
|
|
||||||
for(let layer of json.vector_layers) {
|
|
||||||
sources[key].layers.push(layer.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("Updating source: "+key);
|
|
||||||
this.setState({
|
|
||||||
sources: sources
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Failed to process sources for '%s'", url, err);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isEqual(this.state.sources, sourceList)) {
|
|
||||||
console.debug("Setting sources");
|
|
||||||
this.setState({
|
|
||||||
sources: sourceList
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mapRenderer() {
|
|
||||||
const mapProps = {
|
|
||||||
mapStyle: style.replaceAccessToken(this.state.mapStyle),
|
|
||||||
onDataChange: (e) => {
|
|
||||||
this.layerWatcher.analyzeMap(e.map)
|
|
||||||
this.fetchSources();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
|
||||||
|
|
||||||
// Check if OL3 code has been loaded?
|
|
||||||
if(renderer === 'ol3') {
|
|
||||||
return <OpenLayers3Map {...mapProps} />
|
|
||||||
} else {
|
|
||||||
return <MapboxGlMap {...mapProps}
|
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
|
||||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
|
||||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerSelect(layerId) {
|
|
||||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
|
||||||
this.setState({ selectedLayerIndex: idx })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const layers = this.state.mapStyle.layers || []
|
|
||||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
|
|
||||||
const toolbar = <Toolbar
|
|
||||||
mapStyle={this.state.mapStyle}
|
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
|
||||||
sources={this.state.sources}
|
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
|
||||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
|
||||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const layerList = <LayerList
|
|
||||||
onLayersChange={this.onLayersChange.bind(this)}
|
|
||||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
|
||||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
|
||||||
layers={layers}
|
|
||||||
sources={this.state.sources}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const layerEditor = selectedLayer ? <LayerEditor
|
|
||||||
layer={selectedLayer}
|
|
||||||
sources={this.state.sources}
|
|
||||||
vectorLayers={this.state.vectorLayers}
|
|
||||||
spec={this.state.spec}
|
|
||||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
|
||||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
|
||||||
/> : null
|
|
||||||
|
|
||||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
|
||||||
errors={this.state.errors}
|
|
||||||
infos={this.state.infos}
|
|
||||||
/> : null
|
|
||||||
|
|
||||||
return <AppLayout
|
|
||||||
toolbar={toolbar}
|
|
||||||
layerList={layerList}
|
|
||||||
layerEditor={layerEditor}
|
|
||||||
map={this.mapRenderer()}
|
|
||||||
bottom={bottomPanel}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import ScrollContainer from './ScrollContainer'
|
|
||||||
|
|
||||||
class AppLayout extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
toolbar: PropTypes.element.isRequired,
|
|
||||||
layerList: PropTypes.element.isRequired,
|
|
||||||
layerEditor: PropTypes.element,
|
|
||||||
map: PropTypes.element.isRequired,
|
|
||||||
bottom: PropTypes.element,
|
|
||||||
}
|
|
||||||
|
|
||||||
static childContextTypes = {
|
|
||||||
reactIconBase: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
reactIconBase: { size: 14 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-layout">
|
|
||||||
{this.props.toolbar}
|
|
||||||
<div className="maputnik-layout-list">
|
|
||||||
<ScrollContainer>
|
|
||||||
{this.props.layerList}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-layout-drawer">
|
|
||||||
<ScrollContainer>
|
|
||||||
{this.props.layerEditor}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
|
||||||
{this.props.map}
|
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
|
||||||
{this.props.bottom}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppLayout
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
class Button extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
style: PropTypes.object,
|
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
className={classnames("maputnik-button", this.props.className)}
|
|
||||||
style={this.props.style}>
|
|
||||||
{this.props.children}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Button
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
class MessagePanel extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
errors: PropTypes.array,
|
|
||||||
infos: PropTypes.array,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const errors = this.props.errors.map((m, i) => {
|
|
||||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
|
||||||
})
|
|
||||||
|
|
||||||
const infos = this.props.infos.map((m, i) => {
|
|
||||||
return <p key={"info-"+i}>{m}</p>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-message-panel">
|
|
||||||
{errors}
|
|
||||||
{infos}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default MessagePanel
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
class ScrollContainer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-scroll-container">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScrollContainer
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
|
||||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
|
||||||
import OpenIcon from 'react-icons/lib/md/open-in-browser'
|
|
||||||
import SettingsIcon from 'react-icons/lib/md/settings'
|
|
||||||
import MdInfo from 'react-icons/lib/md/info'
|
|
||||||
import SourcesIcon from 'react-icons/lib/md/layers'
|
|
||||||
import MdSave from 'react-icons/lib/md/save'
|
|
||||||
import MdStyle from 'react-icons/lib/md/style'
|
|
||||||
import MdMap from 'react-icons/lib/md/map'
|
|
||||||
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
|
||||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
|
||||||
import HelpIcon from 'react-icons/lib/md/help-outline'
|
|
||||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
|
||||||
|
|
||||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
|
||||||
import SettingsModal from './modals/SettingsModal'
|
|
||||||
import ExportModal from './modals/ExportModal'
|
|
||||||
import SourcesModal from './modals/SourcesModal'
|
|
||||||
import OpenModal from './modals/OpenModal'
|
|
||||||
|
|
||||||
import style from '../libs/style'
|
|
||||||
|
|
||||||
class IconText extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToolbarLink extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
href: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
|
||||||
href={this.props.href}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToolbarAction extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
onClick: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
className='maputnik-toolbar-action'
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Toolbar extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
|
||||||
onStyleChanged: PropTypes.func.isRequired,
|
|
||||||
// A new style has been uploaded
|
|
||||||
onStyleOpen: PropTypes.func.isRequired,
|
|
||||||
// A dict of source id's and the available source layers
|
|
||||||
sources: PropTypes.object.isRequired,
|
|
||||||
onInspectModeToggle: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.node
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
isOpen: {
|
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
add: false,
|
|
||||||
export: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleModal(modalName) {
|
|
||||||
this.setState({
|
|
||||||
isOpen: {
|
|
||||||
...this.state.isOpen,
|
|
||||||
[modalName]: !this.state.isOpen[modalName]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className='maputnik-toolbar'>
|
|
||||||
<SettingsModal
|
|
||||||
mapStyle={this.props.mapStyle}
|
|
||||||
onStyleChanged={this.props.onStyleChanged}
|
|
||||||
isOpen={this.state.isOpen.settings}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
|
||||||
/>
|
|
||||||
<ExportModal
|
|
||||||
mapStyle={this.props.mapStyle}
|
|
||||||
onStyleChanged={this.props.onStyleChanged}
|
|
||||||
isOpen={this.state.isOpen.export}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
|
||||||
/>
|
|
||||||
<OpenModal
|
|
||||||
isOpen={this.state.isOpen.open}
|
|
||||||
onStyleOpen={this.props.onStyleOpen}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
|
||||||
/>
|
|
||||||
<SourcesModal
|
|
||||||
mapStyle={this.props.mapStyle}
|
|
||||||
onStyleChanged={this.props.onStyleChanged}
|
|
||||||
isOpen={this.state.isOpen.sources}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-toolbar__inner">
|
|
||||||
<ToolbarLink
|
|
||||||
href={"https://github.com/maputnik/editor"}
|
|
||||||
className="maputnik-toolbar-logo"
|
|
||||||
>
|
|
||||||
<img src={logoImage} alt="Maputnik" />
|
|
||||||
<h1>Maputnik</h1>
|
|
||||||
</ToolbarLink>
|
|
||||||
<div className="maputnik-toolbar__actions">
|
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
|
||||||
<OpenIcon />
|
|
||||||
<IconText>Open</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
|
|
||||||
<MdFileDownload />
|
|
||||||
<IconText>Export</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
|
||||||
<SourcesIcon />
|
|
||||||
<IconText>Sources</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
|
||||||
<SettingsIcon />
|
|
||||||
<IconText>Style Settings</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction onClick={this.props.onInspectModeToggle}>
|
|
||||||
<InspectionIcon />
|
|
||||||
<IconText>
|
|
||||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
|
||||||
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
|
|
||||||
</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
|
||||||
<HelpIcon />
|
|
||||||
<IconText>Help</IconText>
|
|
||||||
</ToolbarLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Color from 'color'
|
|
||||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
function formatColor(color) {
|
|
||||||
const rgb = color.rgb
|
|
||||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
|
||||||
class ColorField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
doc: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
default: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
pickerOpened: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: I much rather would do this with absolute positioning
|
|
||||||
//but I am too stupid to get it to work together with fixed position
|
|
||||||
//and scrollbars so I have to fallback to JavaScript
|
|
||||||
calcPickerOffset() {
|
|
||||||
const elem = this.colorInput
|
|
||||||
if(elem) {
|
|
||||||
const pos = elem.getBoundingClientRect()
|
|
||||||
return {
|
|
||||||
top: pos.top,
|
|
||||||
left: pos.left + 196,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
top: 160,
|
|
||||||
left: 555,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePicker() {
|
|
||||||
this.setState({ pickerOpened: !this.state.pickerOpened })
|
|
||||||
}
|
|
||||||
|
|
||||||
get color() {
|
|
||||||
// Catch invalid color.
|
|
||||||
try {
|
|
||||||
return Color(this.props.value).rgb()
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
console.warn("Error parsing color: ", err);
|
|
||||||
return Color("rgb(255,255,255)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const offset = this.calcPickerOffset()
|
|
||||||
var currentColor = this.color.object()
|
|
||||||
currentColor = {
|
|
||||||
r: currentColor.r,
|
|
||||||
g: currentColor.g,
|
|
||||||
b: currentColor.b,
|
|
||||||
// Rename alpha -> a for ChromePicker
|
|
||||||
a: currentColor.alpha
|
|
||||||
}
|
|
||||||
|
|
||||||
const picker = <div
|
|
||||||
className="maputnik-color-picker-offset"
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
zIndex: 1,
|
|
||||||
left: offset.left,
|
|
||||||
top: offset.top,
|
|
||||||
}}>
|
|
||||||
<ChromePicker
|
|
||||||
color={currentColor}
|
|
||||||
onChange={c => this.props.onChange(formatColor(c))}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="maputnik-color-picker-offset"
|
|
||||||
onClick={this.togglePicker.bind(this)}
|
|
||||||
style={{
|
|
||||||
zIndex: -1,
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0px',
|
|
||||||
right: '0px',
|
|
||||||
bottom: '0px',
|
|
||||||
left: '0px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
var swatchStyle = {
|
|
||||||
backgroundColor: this.props.value
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className="maputnik-color-wrapper">
|
|
||||||
{this.state.pickerOpened && picker}
|
|
||||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
|
||||||
<input
|
|
||||||
className="maputnik-color"
|
|
||||||
ref={(input) => this.colorInput = input}
|
|
||||||
onClick={this.togglePicker.bind(this)}
|
|
||||||
style={this.props.style}
|
|
||||||
name={this.props.name}
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={this.props.value ? this.props.value : ""}
|
|
||||||
onChange={(e) => this.props.onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ColorField
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
export default class DocLabel extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
label: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.string
|
|
||||||
]).isRequired,
|
|
||||||
doc: PropTypes.string.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <label className="maputnik-doc-wrapper">
|
|
||||||
<div className="maputnik-doc-target">
|
|
||||||
<span>{this.props.label}</span>
|
|
||||||
<div className="maputnik-doc-popup">
|
|
||||||
{this.props.doc}
|
|
||||||
</div>
|
|
||||||
</div >
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import SpecProperty from './_SpecProperty'
|
|
||||||
import DataProperty from './_DataProperty'
|
|
||||||
import ZoomProperty from './_ZoomProperty'
|
|
||||||
|
|
||||||
|
|
||||||
function isZoomField(value) {
|
|
||||||
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDataField(value) {
|
|
||||||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
|
||||||
*/
|
|
||||||
export default class FunctionSpecProperty extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
fieldName: PropTypes.string.isRequired,
|
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.array
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
addStop() {
|
|
||||||
const stops = this.props.value.stops.slice(0)
|
|
||||||
const lastStop = stops[stops.length - 1]
|
|
||||||
if (typeof lastStop[0] === "object") {
|
|
||||||
stops.push([
|
|
||||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
|
||||||
lastStop[1]
|
|
||||||
])
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
|
||||||
}
|
|
||||||
|
|
||||||
const changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: stops,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteStop(stopIdx) {
|
|
||||||
const stops = this.props.value.stops.slice(0)
|
|
||||||
stops.splice(stopIdx, 1)
|
|
||||||
|
|
||||||
let changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: stops,
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stops.length === 1) {
|
|
||||||
changedValue = stops[0][1]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeZoomFunction() {
|
|
||||||
const zoomFunc = {
|
|
||||||
stops: [
|
|
||||||
[6, this.props.value],
|
|
||||||
[10, this.props.value]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDataFunction() {
|
|
||||||
const dataFunc = {
|
|
||||||
property: "",
|
|
||||||
type: "categorical",
|
|
||||||
stops: [
|
|
||||||
[{zoom: 6, value: 0}, this.props.value],
|
|
||||||
[{zoom: 10, value: 0}, this.props.value]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, dataFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
|
||||||
let specField;
|
|
||||||
|
|
||||||
if (isZoomField(this.props.value)) {
|
|
||||||
specField = (
|
|
||||||
<ZoomProperty
|
|
||||||
onChange={this.props.onChange.bind(this)}
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value}
|
|
||||||
onDeleteStop={this.deleteStop.bind(this)}
|
|
||||||
onAddStop={this.addStop.bind(this)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (isDataField(this.props.value)) {
|
|
||||||
specField = (
|
|
||||||
<DataProperty
|
|
||||||
onChange={this.props.onChange.bind(this)}
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value}
|
|
||||||
onDeleteStop={this.deleteStop.bind(this)}
|
|
||||||
onAddStop={this.addStop.bind(this)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
specField = (
|
|
||||||
<SpecProperty
|
|
||||||
onChange={this.props.onChange.bind(this)}
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value}
|
|
||||||
onZoomClick={this.makeZoomFunction.bind(this)}
|
|
||||||
onDataClick={this.makeDataFunction.bind(this)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={propClass}>
|
|
||||||
{specField}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import FunctionSpecField from './FunctionSpecField'
|
|
||||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
|
||||||
|
|
||||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
|
||||||
* style specification from either the paint or layout group */
|
|
||||||
function getFieldSpec(spec, layerType, fieldName) {
|
|
||||||
const groupName = getGroupName(spec, layerType, fieldName)
|
|
||||||
const group = spec[groupName + '_' + layerType]
|
|
||||||
const fieldSpec = group[fieldName]
|
|
||||||
if(iconProperties.indexOf(fieldName) >= 0) {
|
|
||||||
return {
|
|
||||||
...fieldSpec,
|
|
||||||
values: spec.$root.sprite.values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(fieldName === 'text-font') {
|
|
||||||
return {
|
|
||||||
...fieldSpec,
|
|
||||||
values: spec.$root.glyphs.values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fieldSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGroupName(spec, layerType, fieldName) {
|
|
||||||
const paint = spec['paint_' + layerType] || {}
|
|
||||||
if (fieldName in paint) {
|
|
||||||
return 'paint'
|
|
||||||
} else {
|
|
||||||
return 'layout'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PropertyGroup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
layer: PropTypes.object.isRequired,
|
|
||||||
groupFields: PropTypes.array.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
spec: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
onPropertyChange(property, newValue) {
|
|
||||||
const group = getGroupName(this.props.spec, this.props.layer.type, property)
|
|
||||||
this.props.onChange(group , property, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const fields = this.props.groupFields.map(fieldName => {
|
|
||||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
|
||||||
|
|
||||||
const paint = this.props.layer.paint || {}
|
|
||||||
const layout = this.props.layer.layout || {}
|
|
||||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
|
||||||
|
|
||||||
return <FunctionSpecField
|
|
||||||
onChange={this.onPropertyChange.bind(this)}
|
|
||||||
key={fieldName}
|
|
||||||
fieldName={fieldName}
|
|
||||||
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
|
||||||
fieldSpec={fieldSpec}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-property-group">
|
|
||||||
{fields}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import color from 'color'
|
|
||||||
|
|
||||||
import ColorField from './ColorField'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
import CheckboxInput from '../inputs/CheckboxInput'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
|
||||||
import ArrayInput from '../inputs/ArrayInput'
|
|
||||||
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
|
||||||
import FontInput from '../inputs/FontInput'
|
|
||||||
import IconInput from '../inputs/IconInput'
|
|
||||||
import capitalize from 'lodash.capitalize'
|
|
||||||
|
|
||||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
|
||||||
|
|
||||||
function labelFromFieldName(fieldName) {
|
|
||||||
let label = fieldName.split('-').slice(1).join(' ')
|
|
||||||
if(label.length > 0) {
|
|
||||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
||||||
}
|
|
||||||
return label
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionsLabelLength(options) {
|
|
||||||
let sum = 0;
|
|
||||||
options.forEach(([_, label]) => {
|
|
||||||
sum += label.length
|
|
||||||
})
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Display any field from the Mapbox GL style spec and
|
|
||||||
* choose the correct field component based on the @{fieldSpec}
|
|
||||||
* to display @{value}. */
|
|
||||||
export default class SpecField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
fieldName: PropTypes.string.isRequired,
|
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.array,
|
|
||||||
PropTypes.bool
|
|
||||||
]),
|
|
||||||
/** Override the style of the field */
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const commonProps = {
|
|
||||||
style: this.props.style,
|
|
||||||
value: this.props.value,
|
|
||||||
default: this.props.fieldSpec.default,
|
|
||||||
name: this.props.fieldName,
|
|
||||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
|
||||||
}
|
|
||||||
switch(this.props.fieldSpec.type) {
|
|
||||||
case 'number': return (
|
|
||||||
<NumberInput
|
|
||||||
{...commonProps}
|
|
||||||
min={this.props.fieldSpec.minimum}
|
|
||||||
max={this.props.fieldSpec.maximum}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'enum':
|
|
||||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
|
||||||
|
|
||||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
|
||||||
return <MultiButtonInput
|
|
||||||
{...commonProps}
|
|
||||||
options={options}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <SelectInput
|
|
||||||
{...commonProps}
|
|
||||||
options={options}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
case 'string':
|
|
||||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
|
||||||
return <IconInput
|
|
||||||
{...commonProps}
|
|
||||||
icons={this.props.fieldSpec.values}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <StringInput
|
|
||||||
{...commonProps}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
case 'color': return (
|
|
||||||
<ColorField
|
|
||||||
{...commonProps}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'boolean': return (
|
|
||||||
<CheckboxInput
|
|
||||||
{...commonProps}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'array':
|
|
||||||
if(this.props.fieldName === 'text-font') {
|
|
||||||
return <FontInput
|
|
||||||
{...commonProps}
|
|
||||||
fonts={this.props.fieldSpec.values}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
if (this.props.fieldSpec.length) {
|
|
||||||
return <ArrayInput
|
|
||||||
{...commonProps}
|
|
||||||
type={this.props.fieldSpec.value}
|
|
||||||
length={this.props.fieldSpec.length}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <DynamicArrayInput
|
|
||||||
{...commonProps}
|
|
||||||
type={this.props.fieldSpec.value}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import Button from '../Button'
|
|
||||||
import SpecField from './SpecField'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
|
|
||||||
import labelFromFieldName from './_labelFromFieldName'
|
|
||||||
import DeleteStopButton from './_DeleteStopButton'
|
|
||||||
|
|
||||||
|
|
||||||
export default class DataProperty extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onDeleteStop: PropTypes.func,
|
|
||||||
onAddStop: PropTypes.func,
|
|
||||||
fieldName: PropTypes.string,
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.array
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec) {
|
|
||||||
if (fieldSpec.function === "interpolated") {
|
|
||||||
return "exponential"
|
|
||||||
}
|
|
||||||
if (fieldSpec.type === "number") {
|
|
||||||
return "interval"
|
|
||||||
}
|
|
||||||
return "categorical"
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataFunctionTypes(functionType) {
|
|
||||||
if (functionType === "interpolated") {
|
|
||||||
return ["categorical", "interval", "exponential"]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ["categorical", "interval"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
changeStop(changeIdx, stopData, value) {
|
|
||||||
const stops = this.props.value.stops.slice(0)
|
|
||||||
stops[changeIdx] = [stopData, value]
|
|
||||||
const changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: stops,
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeDataProperty(propName, propVal) {
|
|
||||||
if (propVal) {
|
|
||||||
this.props.value[propName] = propVal
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delete this.props.value[propName]
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, this.props.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (typeof this.props.value.type === "undefined") {
|
|
||||||
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataFields = this.props.value.stops.map((stop, idx) => {
|
|
||||||
const zoomLevel = stop[0].zoom
|
|
||||||
const dataLevel = stop[0].value
|
|
||||||
const value = stop[1]
|
|
||||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
|
||||||
|
|
||||||
const dataProps = {
|
|
||||||
label: "Data value",
|
|
||||||
value: dataLevel,
|
|
||||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataInput;
|
|
||||||
if(this.props.value.type === "categorical") {
|
|
||||||
dataInput = <StringInput {...dataProps} />
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dataInput = <NumberInput {...dataProps} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
|
||||||
<div className="maputnik-data-spec-property-stop-edit">
|
|
||||||
<NumberInput
|
|
||||||
value={zoomLevel}
|
|
||||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
|
||||||
min={0}
|
|
||||||
max={22}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-stop-data">
|
|
||||||
{dataInput}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-stop-value">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={value}
|
|
||||||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-data-spec-block">
|
|
||||||
<div className="maputnik-data-spec-property">
|
|
||||||
<InputBlock
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Property"
|
|
||||||
doc={"Input a data property to base styles off of."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<StringInput
|
|
||||||
value={this.props.value.property}
|
|
||||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Type"
|
|
||||||
doc={"Select a type of data scale (default is 'categorical')."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SelectInput
|
|
||||||
value={this.props.value.type}
|
|
||||||
onChange={propVal => this.changeDataProperty("type", propVal)}
|
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec.function)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Default"
|
|
||||||
doc={"Input a default value for data if not covered by the scales."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value.default}
|
|
||||||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
</div>
|
|
||||||
{dataFields}
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-stop"
|
|
||||||
onClick={this.props.onAddStop.bind(this)}
|
|
||||||
>
|
|
||||||
Add stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import Button from '../Button'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
|
|
||||||
export default class DeleteStopButton extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Button
|
|
||||||
className="maputnik-delete-stop"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<DeleteIcon />}
|
|
||||||
doc={"Remove zoom level stop."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import Button from '../Button'
|
|
||||||
import FunctionIcon from 'react-icons/lib/md/functions'
|
|
||||||
import MdInsertChart from 'react-icons/lib/md/insert-chart'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FunctionButtons extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
onZoomClick: PropTypes.func,
|
|
||||||
onDataClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let makeZoomButton, makeDataButton
|
|
||||||
if (this.props.fieldSpec['zoom-function']) {
|
|
||||||
makeZoomButton = <Button
|
|
||||||
className="maputnik-make-zoom-function"
|
|
||||||
onClick={this.props.onZoomClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<FunctionIcon />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
|
||||||
makeDataButton = <Button
|
|
||||||
className="maputnik-make-data-function"
|
|
||||||
onClick={this.props.onDataClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<MdInsertChart />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import SpecField from './SpecField'
|
|
||||||
import FunctionButtons from './_FunctionButtons'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
|
|
||||||
import labelFromFieldName from './_labelFromFieldName'
|
|
||||||
|
|
||||||
|
|
||||||
export default class SpecProperty extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onZoomClick: PropTypes.func.isRequired,
|
|
||||||
onDataClick: PropTypes.func.isRequired,
|
|
||||||
fieldName: PropTypes.string,
|
|
||||||
fieldSpec: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const functionBtn = <FunctionButtons
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
onZoomClick={this.props.onZoomClick}
|
|
||||||
onDataClick={this.props.onDataClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
return <InputBlock
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
action={functionBtn}
|
|
||||||
>
|
|
||||||
<SpecField {...this.props} />
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import Button from '../Button'
|
|
||||||
import SpecField from './SpecField'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
|
|
||||||
import DeleteStopButton from './_DeleteStopButton'
|
|
||||||
import labelFromFieldName from './_labelFromFieldName'
|
|
||||||
|
|
||||||
import docUid from '../../libs/document-uid'
|
|
||||||
import sortNumerically from '../../libs/sort-numerically'
|
|
||||||
|
|
||||||
|
|
||||||
export default class ZoomProperty extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onDeleteStop: PropTypes.func,
|
|
||||||
onAddStop: PropTypes.func,
|
|
||||||
fieldName: PropTypes.string,
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.array
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.state = {
|
|
||||||
refs: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.setState({
|
|
||||||
refs: this.setStopRefs(this.props)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We cache a reference for each stop by its index.
|
|
||||||
*
|
|
||||||
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
|
||||||
*/
|
|
||||||
setStopRefs(props) {
|
|
||||||
// This is initialsed below only if required to improved performance.
|
|
||||||
let newRefs;
|
|
||||||
|
|
||||||
if(props.value && props.value.stops) {
|
|
||||||
props.value.stops.forEach((val, idx) => {
|
|
||||||
if(!this.state.refs.hasOwnProperty(idx)) {
|
|
||||||
if(!newRefs) {
|
|
||||||
newRefs = {...this.state.refs};
|
|
||||||
}
|
|
||||||
newRefs[idx] = docUid("stop-");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
const newRefs = this.setStopRefs(nextProps);
|
|
||||||
if(newRefs) {
|
|
||||||
this.setState({
|
|
||||||
refs: newRefs
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order the stops altering the refs to reflect their new position.
|
|
||||||
orderStopsByZoom(stops) {
|
|
||||||
const mappedWithRef = stops
|
|
||||||
.map((stop, idx) => {
|
|
||||||
return {
|
|
||||||
ref: this.state.refs[idx],
|
|
||||||
data: stop
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Sort by zoom
|
|
||||||
.sort((a, b) => sortNumerically(a.data[0], b.data[0]));
|
|
||||||
|
|
||||||
// Fetch the new position of the stops
|
|
||||||
const newRefs = {};
|
|
||||||
mappedWithRef
|
|
||||||
.forEach((stop, idx) =>{
|
|
||||||
newRefs[idx] = stop.ref;
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
refs: newRefs
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedWithRef.map((item) => item.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeZoomStop(changeIdx, stopData, value) {
|
|
||||||
const stops = this.props.value.stops.slice(0);
|
|
||||||
stops[changeIdx] = [stopData, value];
|
|
||||||
|
|
||||||
const orderedStops = this.orderStopsByZoom(stops);
|
|
||||||
|
|
||||||
const changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: orderedStops
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
|
||||||
const zoomLevel = stop[0]
|
|
||||||
const key = this.state.refs[idx];
|
|
||||||
const value = stop[1]
|
|
||||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
|
||||||
|
|
||||||
return <InputBlock
|
|
||||||
key={key}
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
action={deleteStopBtn}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
|
||||||
<NumberInput
|
|
||||||
value={zoomLevel}
|
|
||||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
|
||||||
min={0}
|
|
||||||
max={22}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-zoom-spec-property-stop-value">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={value}
|
|
||||||
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className="maputnik-zoom-spec-property">
|
|
||||||
{zoomFields}
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-stop"
|
|
||||||
onClick={this.props.onAddStop.bind(this)}
|
|
||||||
>
|
|
||||||
Add stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import capitalize from 'lodash.capitalize'
|
|
||||||
|
|
||||||
export default function labelFromFieldName(fieldName) {
|
|
||||||
let label = fieldName.split('-').slice(1).join(' ')
|
|
||||||
return capitalize(label)
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import DocLabel from '../fields/DocLabel'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
|
||||||
import FilterEditorBlock from './FilterEditorBlock'
|
|
||||||
import Button from '../Button'
|
|
||||||
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
import AddIcon from 'react-icons/lib/fa/plus'
|
|
||||||
|
|
||||||
function hasCombiningFilter(filter) {
|
|
||||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasNestedCombiningFilter(filter) {
|
|
||||||
if(hasCombiningFilter(filter)) {
|
|
||||||
const combinedFilters = filter.slice(1)
|
|
||||||
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CombiningFilterEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
/** Properties of the vector layer and the available fields */
|
|
||||||
properties: PropTypes.object,
|
|
||||||
filter: PropTypes.array,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert filter to combining filter
|
|
||||||
combiningFilter() {
|
|
||||||
let filter = this.props.filter || ['all']
|
|
||||||
|
|
||||||
let combiningOp = filter[0]
|
|
||||||
let filters = filter.slice(1)
|
|
||||||
|
|
||||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
|
||||||
combiningOp = 'all'
|
|
||||||
filters = [filter.slice(0)]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [combiningOp, ...filters]
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterPartChanged(filterIdx, newPart) {
|
|
||||||
const newFilter = this.combiningFilter().slice(0)
|
|
||||||
newFilter[filterIdx] = newPart
|
|
||||||
this.props.onChange(newFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFilterItem(filterIdx) {
|
|
||||||
const newFilter = this.combiningFilter().slice(0)
|
|
||||||
console.log('Delete', filterIdx, newFilter)
|
|
||||||
newFilter.splice(filterIdx + 1, 1)
|
|
||||||
this.props.onChange(newFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
addFilterItem() {
|
|
||||||
const newFilterItem = this.combiningFilter().slice(0)
|
|
||||||
newFilterItem.push(['==', 'name', ''])
|
|
||||||
this.props.onChange(newFilterItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const filter = this.combiningFilter()
|
|
||||||
let combiningOp = filter[0]
|
|
||||||
let filters = filter.slice(1)
|
|
||||||
|
|
||||||
const editorBlocks = filters.map((f, idx) => {
|
|
||||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
|
||||||
<SingleFilterEditor
|
|
||||||
properties={this.props.properties}
|
|
||||||
filter={f}
|
|
||||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
|
||||||
/>
|
|
||||||
</FilterEditorBlock>
|
|
||||||
})
|
|
||||||
|
|
||||||
//TODO: Implement support for nested filter
|
|
||||||
if(hasNestedCombiningFilter(filter)) {
|
|
||||||
return <div className="maputnik-filter-editor-unsupported">
|
|
||||||
Nested filters are not supported.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="maputnik-filter-editor">
|
|
||||||
<div className="maputnik-filter-editor-compound-select">
|
|
||||||
<DocLabel
|
|
||||||
label={"Compound Filter"}
|
|
||||||
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
|
||||||
/>
|
|
||||||
<SelectInput
|
|
||||||
value={combiningOp}
|
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
|
||||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{editorBlocks}
|
|
||||||
<div className="maputnik-filter-editor-add-wrapper">
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-filter"
|
|
||||||
onClick={this.addFilterItem.bind(this)}>
|
|
||||||
Add filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Button from '../Button'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
class FilterEditorBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onDelete: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.element.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-filter-editor-block">
|
|
||||||
<div className="maputnik-filter-editor-block-action">
|
|
||||||
<Button
|
|
||||||
className="maputnik-delete-filter"
|
|
||||||
onClick={this.props.onDelete}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-filter-editor-block-content">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FilterEditorBlock
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import { otherFilterOps } from '../../libs/filterops.js'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
|
|
||||||
function tryParseInt(v) {
|
|
||||||
if (v === '') return v
|
|
||||||
if (isNaN(v)) return v
|
|
||||||
return parseFloat(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryParseBool(v) {
|
|
||||||
const isString = (typeof(v) === "string");
|
|
||||||
if(!isString) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(v.match(/^\s*true\s*$/)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if(v.match(/^\s*false\s*$/)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFilter(v) {
|
|
||||||
v = tryParseInt(v);
|
|
||||||
v = tryParseBool(v);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SingleFilterEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
filter: PropTypes.array.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
properties: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
properties: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
|
||||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
|
|
||||||
if(filterOp === 'has' || filterOp === '!has') {
|
|
||||||
newFilter = [filterOp, propertyName]
|
|
||||||
} else if(filterArgs.length === 0) {
|
|
||||||
newFilter = [filterOp, propertyName, '']
|
|
||||||
}
|
|
||||||
this.props.onChange(newFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const f = this.props.filter
|
|
||||||
const filterOp = f[0]
|
|
||||||
const propertyName = f[1]
|
|
||||||
const filterArgs = f.slice(2)
|
|
||||||
|
|
||||||
return <div className="maputnik-filter-editor-single">
|
|
||||||
<div className="maputnik-filter-editor-property">
|
|
||||||
<AutocompleteInput
|
|
||||||
value={propertyName}
|
|
||||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
|
||||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-filter-editor-operator">
|
|
||||||
<SelectInput
|
|
||||||
value={filterOp}
|
|
||||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
|
||||||
options={otherFilterOps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{filterArgs.length > 0 &&
|
|
||||||
<div className="maputnik-filter-editor-args">
|
|
||||||
<StringInput
|
|
||||||
value={filterArgs.join(',')}
|
|
||||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SingleFilterEditor
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import IconBase from 'react-icon-base'
|
|
||||||
|
|
||||||
|
|
||||||
export default class BackgroundIcon extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
|
||||||
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
|
|
||||||
</IconBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import IconBase from 'react-icon-base'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FillIcon extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
|
||||||
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
|
|
||||||
</IconBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import IconBase from 'react-icon-base'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FillIcon extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
|
||||||
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
|
|
||||||
</IconBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import LineIcon from './LineIcon.jsx'
|
|
||||||
import FillIcon from './FillIcon.jsx'
|
|
||||||
import SymbolIcon from './SymbolIcon.jsx'
|
|
||||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
|
||||||
import CircleIcon from './CircleIcon.jsx'
|
|
||||||
|
|
||||||
class LayerIcon extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const iconProps = { style: this.props.style }
|
|
||||||
switch(this.props.type) {
|
|
||||||
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
|
||||||
case 'raster': return <FillIcon {...iconProps} />
|
|
||||||
case 'fill': return <FillIcon {...iconProps} />
|
|
||||||
case 'background': return <BackgroundIcon {...iconProps} />
|
|
||||||
case 'line': return <LineIcon {...iconProps} />
|
|
||||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
|
||||||
case 'circle': return <CircleIcon {...iconProps} />
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerIcon
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import IconBase from 'react-icon-base'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FillIcon extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
|
||||||
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
|
|
||||||
</IconBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import IconBase from 'react-icon-base'
|
|
||||||
|
|
||||||
|
|
||||||
export default class SymbolIcon extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
|
||||||
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
|
||||||
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
|
||||||
</g>
|
|
||||||
</IconBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import StringInput from './StringInput'
|
|
||||||
import NumberInput from './NumberInput'
|
|
||||||
|
|
||||||
class ArrayInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.array,
|
|
||||||
type: PropTypes.string,
|
|
||||||
length: PropTypes.number,
|
|
||||||
default: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
|
||||||
console.log(idx, newValue)
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
values[idx] = newValue
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
get values() {
|
|
||||||
return this.props.value || this.props.default || []
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputs = this.values.map((v, i) => {
|
|
||||||
if(this.props.type === 'number') {
|
|
||||||
return <NumberInput
|
|
||||||
key={i}
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <StringInput
|
|
||||||
key={i}
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
|
||||||
{inputs}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ArrayInput
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import Autocomplete from 'react-autocomplete'
|
|
||||||
|
|
||||||
|
|
||||||
const MAX_HEIGHT = 140;
|
|
||||||
|
|
||||||
class AutocompleteInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
options: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
keepMenuWithinWindowBounds: PropTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
options: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
maxHeight: MAX_HEIGHT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
calcMaxHeight() {
|
|
||||||
if(this.props.keepMenuWithinWindowBounds) {
|
|
||||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
|
||||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
|
||||||
|
|
||||||
if(limitedMaxHeight != this.state.maxHeight) {
|
|
||||||
this.setState({
|
|
||||||
maxHeight: limitedMaxHeight
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
this.calcMaxHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.calcMaxHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
ref={(el) => {
|
|
||||||
this.autocompleteMenuEl = el;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Autocomplete
|
|
||||||
menuStyle={{
|
|
||||||
position: "absolute",
|
|
||||||
overflow: "auto",
|
|
||||||
maxHeight: this.state.maxHeight
|
|
||||||
}}
|
|
||||||
wrapperProps={{
|
|
||||||
className: "maputnik-autocomplete",
|
|
||||||
style: null
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
className: "maputnik-string"
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
items={this.props.options}
|
|
||||||
getItemValue={(item) => item[0]}
|
|
||||||
onSelect={v => this.props.onChange(v)}
|
|
||||||
onChange={(e, v) => this.props.onChange(v)}
|
|
||||||
shouldItemRender={(item, value) => {
|
|
||||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
|
||||||
}}
|
|
||||||
renderItem={(item, isHighlighted) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-autocomplete-menu-item": true,
|
|
||||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutocompleteInput
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
class CheckboxInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.bool.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <label className="maputnik-checkbox-wrapper">
|
|
||||||
<input
|
|
||||||
className="maputnik-checkbox"
|
|
||||||
type="checkbox"
|
|
||||||
style={this.props.style}
|
|
||||||
onChange={e => this.props.onChange(!this.props.value)}
|
|
||||||
checked={this.props.value}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-checkbox-box">
|
|
||||||
<svg style={{
|
|
||||||
display: this.props.value ? 'inline' : 'none'
|
|
||||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
|
||||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CheckboxInput
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import StringInput from './StringInput'
|
|
||||||
import NumberInput from './NumberInput'
|
|
||||||
import Button from '../Button'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
import DocLabel from '../fields/DocLabel'
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicArrayInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.array,
|
|
||||||
type: PropTypes.string,
|
|
||||||
default: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
|
||||||
console.log(idx, newValue)
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
values[idx] = newValue
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
get values() {
|
|
||||||
return this.props.value || this.props.default || []
|
|
||||||
}
|
|
||||||
|
|
||||||
addValue() {
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
if (this.props.type === 'number') {
|
|
||||||
values.push(0)
|
|
||||||
} else {
|
|
||||||
values.push("")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteValue(valueIdx) {
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
values.splice(valueIdx, 1)
|
|
||||||
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputs = this.values.map((v, i) => {
|
|
||||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
|
||||||
const input = this.props.type === 'number'
|
|
||||||
? <NumberInput
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
: <StringInput
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
return <div
|
|
||||||
style={this.props.style}
|
|
||||||
key={i}
|
|
||||||
className="maputnik-array-block"
|
|
||||||
>
|
|
||||||
<div className="maputnik-array-block-action">
|
|
||||||
{deleteValueBtn}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-array-block-content">
|
|
||||||
{input}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
|
||||||
{inputs}
|
|
||||||
<Button
|
|
||||||
className="maputnik-array-add-value"
|
|
||||||
onClick={this.addValue.bind(this)}
|
|
||||||
>
|
|
||||||
Add value
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteValueButton extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Button
|
|
||||||
className="maputnik-delete-stop"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<DeleteIcon />}
|
|
||||||
doc={"Remove array entry."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DynamicArrayInput
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import AutocompleteInput from './AutocompleteInput'
|
|
||||||
|
|
||||||
class FontInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.array.isRequired,
|
|
||||||
default: PropTypes.array,
|
|
||||||
fonts: PropTypes.array,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
fonts: []
|
|
||||||
}
|
|
||||||
|
|
||||||
get values() {
|
|
||||||
return this.props.value || this.props.default.slice(1) || []
|
|
||||||
}
|
|
||||||
|
|
||||||
changeFont(idx, newValue) {
|
|
||||||
const changedValues = this.values.slice(0)
|
|
||||||
changedValues[idx] = newValue
|
|
||||||
this.props.onChange(changedValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputs = this.values.map((value, i) => {
|
|
||||||
return <AutocompleteInput
|
|
||||||
key={i}
|
|
||||||
value={value}
|
|
||||||
options={this.props.fonts.map(f => [f, f])}
|
|
||||||
onChange={this.changeFont.bind(this, i)}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-font">
|
|
||||||
{inputs}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FontInput
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import AutocompleteInput from './AutocompleteInput'
|
|
||||||
|
|
||||||
|
|
||||||
class IconInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.array,
|
|
||||||
icons: PropTypes.array,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
icons: []
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <AutocompleteInput
|
|
||||||
value={this.props.value}
|
|
||||||
options={this.props.icons.map(f => [f, f])}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
wrapperStyle={this.props.style}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IconInput
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import DocLabel from '../fields/DocLabel'
|
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
|
||||||
class InputBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
label: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.element,
|
|
||||||
]).isRequired,
|
|
||||||
doc: PropTypes.string,
|
|
||||||
action: PropTypes.element,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e) {
|
|
||||||
const value = e.target.value
|
|
||||||
return this.props.onChange(value === "" ? null: value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div style={this.props.style}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-input-block": true,
|
|
||||||
"maputnik-action-block": this.props.action
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{this.props.doc &&
|
|
||||||
<div className="maputnik-input-block-label">
|
|
||||||
<DocLabel
|
|
||||||
label={this.props.label}
|
|
||||||
doc={this.props.doc}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{!this.props.doc &&
|
|
||||||
<label className="maputnik-input-block-label">
|
|
||||||
{this.props.label}
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
{this.props.action &&
|
|
||||||
<div className="maputnik-input-block-action">
|
|
||||||
{this.props.action}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className="maputnik-input-block-content">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InputBlock
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import Button from '../Button'
|
|
||||||
|
|
||||||
class MultiButtonInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
options: PropTypes.array.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let options = this.props.options
|
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
|
||||||
options = options.map(v => [v, v])
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedValue = this.props.value || options[0][0]
|
|
||||||
const buttons = options.map(([val, label])=> {
|
|
||||||
return <Button
|
|
||||||
key={val}
|
|
||||||
onClick={e => this.props.onChange(val)}
|
|
||||||
className={classnames({"maputnik-button-selected": val === selectedValue})}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-multibutton">
|
|
||||||
{buttons}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MultiButtonInput
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
class NumberInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.number,
|
|
||||||
default: PropTypes.number,
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
value: props.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({ value: nextProps.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(newValue) {
|
|
||||||
const value = parseFloat(newValue)
|
|
||||||
|
|
||||||
const hasChanged = this.state.value !== value
|
|
||||||
if(this.isValid(value) && hasChanged) {
|
|
||||||
this.props.onChange(value)
|
|
||||||
} else {
|
|
||||||
this.setState({ value: newValue })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid(v) {
|
|
||||||
const value = parseFloat(v)
|
|
||||||
if(isNaN(value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isNaN(this.props.min) && value < this.props.min) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isNaN(this.props.max) && value > this.props.max) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
resetValue() {
|
|
||||||
// Reset explicitly to default value if value has been cleared
|
|
||||||
if(this.state.value === "") {
|
|
||||||
return this.changeValue(this.props.default)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
|
||||||
if(!this.isValid(this.state.value)) {
|
|
||||||
if(this.isValid(this.props.value)) {
|
|
||||||
this.changeValue(this.props.value)
|
|
||||||
} else {
|
|
||||||
this.changeValue(this.props.default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <input
|
|
||||||
className="maputnik-number"
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={this.state.value}
|
|
||||||
onChange={e => this.changeValue(e.target.value)}
|
|
||||||
onBlur={this.resetValue.bind(this)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NumberInput
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
class SelectInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
options: PropTypes.array.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let options = this.props.options
|
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
|
||||||
options = options.map(v => [v, v])
|
|
||||||
}
|
|
||||||
|
|
||||||
return <select
|
|
||||||
className="maputnik-select"
|
|
||||||
style={this.props.style}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
|
||||||
>
|
|
||||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
|
||||||
</select>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectInput
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
class StringInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
default: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
multi: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
value: props.value || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({ value: nextProps.value || '' })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let tag;
|
|
||||||
let classes;
|
|
||||||
|
|
||||||
if(!!this.props.multi) {
|
|
||||||
tag = "textarea"
|
|
||||||
classes = [
|
|
||||||
"maputnik-string",
|
|
||||||
"maputnik-string--multi"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
tag = "input"
|
|
||||||
classes = [
|
|
||||||
"maputnik-string"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.createElement(tag, {
|
|
||||||
className: classes.join(" "),
|
|
||||||
style: this.props.style,
|
|
||||||
value: this.state.value,
|
|
||||||
placeholder: this.props.default,
|
|
||||||
onChange: e => this.setState({ value: e.target.value }),
|
|
||||||
onBlur: () => {
|
|
||||||
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StringInput
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
|
||||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
|
||||||
|
|
||||||
export default class Collapser extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
isCollapsed: PropTypes.bool.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const iconStyle = {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
...this.props.style,
|
|
||||||
}
|
|
||||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
|
|
||||||
class MetadataBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Comments"} doc={"Comments for the current layer. This is non-standard and not in the spec."}>
|
|
||||||
<StringInput
|
|
||||||
multi={true}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
default="Comment..."
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetadataBlock
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {Controlled as CodeMirror} from 'react-codemirror2'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
|
|
||||||
import 'codemirror/mode/javascript/javascript'
|
|
||||||
import 'codemirror/addon/lint/lint'
|
|
||||||
import 'codemirror/lib/codemirror.css'
|
|
||||||
import 'codemirror/addon/lint/lint.css'
|
|
||||||
import '../../codemirror-maputnik.css'
|
|
||||||
import jsonlint from 'jsonlint'
|
|
||||||
|
|
||||||
// This is mainly because of this issue <https://github.com/zaach/jsonlint/issues/57> also the API has changed, see comment in file
|
|
||||||
import '../../vendor/codemirror/addon/lint/json-lint'
|
|
||||||
|
|
||||||
|
|
||||||
class JSONEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
layer: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
code: JSON.stringify(props.layer, null, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({
|
|
||||||
code: JSON.stringify(nextProps.layer, null, 2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
try {
|
|
||||||
const parsedLayer = JSON.parse(this.state.code)
|
|
||||||
// If the structure is still the same do not update
|
|
||||||
// because it affects editing experience by reformatting all the time
|
|
||||||
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
|
|
||||||
} catch(err) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCodeUpdate(newCode) {
|
|
||||||
try {
|
|
||||||
const parsedLayer = JSON.parse(newCode)
|
|
||||||
this.props.onChange(parsedLayer)
|
|
||||||
} catch(err) {
|
|
||||||
console.warn(err)
|
|
||||||
} finally {
|
|
||||||
this.setState({
|
|
||||||
code: newCode
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetValue() {
|
|
||||||
console.log('reset')
|
|
||||||
this.setState({
|
|
||||||
code: JSON.stringify(this.props.layer, null, 2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const codeMirrorOptions = {
|
|
||||||
mode: {name: "javascript", json: true},
|
|
||||||
tabSize: 2,
|
|
||||||
theme: 'maputnik',
|
|
||||||
viewportMargin: Infinity,
|
|
||||||
lineNumbers: true,
|
|
||||||
lint: true,
|
|
||||||
gutters: ["CodeMirror-lint-markers"],
|
|
||||||
scrollbarStyle: "null",
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CodeMirror
|
|
||||||
value={this.state.code}
|
|
||||||
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
|
|
||||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
|
||||||
options={codeMirrorOptions}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default JSONEditor
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import JSONEditor from './JSONEditor'
|
|
||||||
import FilterEditor from '../filter/FilterEditor'
|
|
||||||
import PropertyGroup from '../fields/PropertyGroup'
|
|
||||||
import LayerEditorGroup from './LayerEditorGroup'
|
|
||||||
import LayerTypeBlock from './LayerTypeBlock'
|
|
||||||
import LayerIdBlock from './LayerIdBlock'
|
|
||||||
import MinZoomBlock from './MinZoomBlock'
|
|
||||||
import MaxZoomBlock from './MaxZoomBlock'
|
|
||||||
import CommentBlock from './CommentBlock'
|
|
||||||
import LayerSourceBlock from './LayerSourceBlock'
|
|
||||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
|
||||||
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
|
||||||
|
|
||||||
import { changeType, changeProperty } from '../../libs/layer'
|
|
||||||
import layout from '../../config/layout.json'
|
|
||||||
|
|
||||||
class UnsupportedLayer extends React.Component {
|
|
||||||
render() {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function layoutGroups(layerType) {
|
|
||||||
const layerGroup = {
|
|
||||||
title: 'Layer',
|
|
||||||
type: 'layer'
|
|
||||||
}
|
|
||||||
const filterGroup = {
|
|
||||||
title: 'Filter',
|
|
||||||
type: 'filter'
|
|
||||||
}
|
|
||||||
const editorGroup = {
|
|
||||||
title: 'JSON Editor',
|
|
||||||
type: 'jsoneditor'
|
|
||||||
}
|
|
||||||
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Layer editor supporting multiple types of layers. */
|
|
||||||
export default class LayerEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
layer: PropTypes.object.isRequired,
|
|
||||||
sources: PropTypes.object,
|
|
||||||
vectorLayers: PropTypes.object,
|
|
||||||
spec: PropTypes.object.isRequired,
|
|
||||||
onLayerChanged: PropTypes.func,
|
|
||||||
onLayerIdChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onLayerChanged: () => {},
|
|
||||||
onLayerIdChange: () => {},
|
|
||||||
onLayerDestroyed: () => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
static childContextTypes = {
|
|
||||||
reactIconBase: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
//TODO: Clean this up and refactor into function
|
|
||||||
const editorGroups = {}
|
|
||||||
layoutGroups(this.props.layer.type).forEach(group => {
|
|
||||||
editorGroups[group.title] = true
|
|
||||||
})
|
|
||||||
|
|
||||||
this.state = { editorGroups }
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
const additionalGroups = { ...this.state.editorGroups }
|
|
||||||
|
|
||||||
layout[nextProps.layer.type].groups.forEach(group => {
|
|
||||||
if(!(group.title in additionalGroups)) {
|
|
||||||
additionalGroups[group.title] = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
editorGroups: additionalGroups
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext () {
|
|
||||||
return {
|
|
||||||
reactIconBase: {
|
|
||||||
size: 14,
|
|
||||||
color: '#8e8e8e',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeProperty(group, property, newValue) {
|
|
||||||
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
onGroupToggle(groupTitle, active) {
|
|
||||||
const changedActiveGroups = {
|
|
||||||
...this.state.editorGroups,
|
|
||||||
[groupTitle]: active,
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
editorGroups: changedActiveGroups
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGroupType(type, fields) {
|
|
||||||
let comment = ""
|
|
||||||
if(this.props.layer.metadata) {
|
|
||||||
comment = this.props.layer.metadata['maputnik:comment']
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(type) {
|
|
||||||
case 'layer': return <div>
|
|
||||||
<LayerIdBlock
|
|
||||||
value={this.props.layer.id}
|
|
||||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
|
||||||
/>
|
|
||||||
<LayerTypeBlock
|
|
||||||
value={this.props.layer.type}
|
|
||||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
|
||||||
/>
|
|
||||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
|
||||||
sourceIds={Object.keys(this.props.sources)}
|
|
||||||
value={this.props.layer.source}
|
|
||||||
onChange={v => this.changeProperty(null, 'source', v)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
|
|
||||||
sourceLayerIds={this.props.sources[this.props.layer.source].layers}
|
|
||||||
value={this.props.layer['source-layer']}
|
|
||||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<MinZoomBlock
|
|
||||||
value={this.props.layer.minzoom}
|
|
||||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
|
||||||
/>
|
|
||||||
<MaxZoomBlock
|
|
||||||
value={this.props.layer.maxzoom}
|
|
||||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
|
||||||
/>
|
|
||||||
<CommentBlock
|
|
||||||
value={comment}
|
|
||||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
case 'filter': return <div>
|
|
||||||
<div className="maputnik-filter-editor-wrapper">
|
|
||||||
<FilterEditor
|
|
||||||
filter={this.props.layer.filter}
|
|
||||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
|
||||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
case 'properties': return <PropertyGroup
|
|
||||||
layer={this.props.layer}
|
|
||||||
groupFields={fields}
|
|
||||||
spec={this.props.spec}
|
|
||||||
onChange={this.changeProperty.bind(this)}
|
|
||||||
/>
|
|
||||||
case 'jsoneditor': return <JSONEditor
|
|
||||||
layer={this.props.layer}
|
|
||||||
onChange={this.props.onLayerChanged}
|
|
||||||
/>
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const layerType = this.props.layer.type
|
|
||||||
const groups = layoutGroups(layerType).filter(group => {
|
|
||||||
return !(layerType === 'background' && group.type === 'source')
|
|
||||||
}).map(group => {
|
|
||||||
return <LayerEditorGroup
|
|
||||||
key={group.title}
|
|
||||||
title={group.title}
|
|
||||||
isActive={this.state.editorGroups[group.title]}
|
|
||||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
|
||||||
>
|
|
||||||
{this.renderGroupType(group.type, group.fields)}
|
|
||||||
</LayerEditorGroup>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-layer-editor"
|
|
||||||
>
|
|
||||||
{groups}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Collapse from 'react-collapse'
|
|
||||||
import Collapser from './Collapser'
|
|
||||||
|
|
||||||
export default class LayerEditorGroup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
isActive: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.element.isRequired,
|
|
||||||
onActiveToggle: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div>
|
|
||||||
<div className="maputnik-layer-editor-group"
|
|
||||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
|
||||||
>
|
|
||||||
<span>{this.props.title}</span>
|
|
||||||
<span style={{flexGrow: 1}} />
|
|
||||||
<Collapser isCollapsed={this.props.isActive} />
|
|
||||||
</div>
|
|
||||||
<Collapse isOpened={this.props.isActive}>
|
|
||||||
{this.props.children}
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
|
|
||||||
class LayerIdBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}>
|
|
||||||
<StringInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerIdBlock
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
|
||||||
|
|
||||||
import Button from '../Button'
|
|
||||||
import LayerListGroup from './LayerListGroup'
|
|
||||||
import LayerListItem from './LayerListItem'
|
|
||||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
|
||||||
import AddModal from '../modals/AddModal'
|
|
||||||
|
|
||||||
import style from '../../libs/style.js'
|
|
||||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
|
||||||
|
|
||||||
const layerListPropTypes = {
|
|
||||||
layers: PropTypes.array.isRequired,
|
|
||||||
selectedLayerIndex: PropTypes.number.isRequired,
|
|
||||||
onLayersChange: PropTypes.func.isRequired,
|
|
||||||
onLayerSelect: PropTypes.func,
|
|
||||||
sources: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
function layerPrefix(name) {
|
|
||||||
return name.replace(' ', '-').replace('_', '-').split('-')[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function findClosestCommonPrefix(layers, idx) {
|
|
||||||
const currentLayerPrefix = layerPrefix(layers[idx].id)
|
|
||||||
let closestIdx = idx
|
|
||||||
for (let i = idx; i > 0; i--) {
|
|
||||||
const previousLayerPrefix = layerPrefix(layers[i-1].id)
|
|
||||||
if(previousLayerPrefix === currentLayerPrefix) {
|
|
||||||
closestIdx = i - 1
|
|
||||||
} else {
|
|
||||||
return closestIdx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return closestIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of collapsible layer editors
|
|
||||||
@SortableContainer
|
|
||||||
class LayerListContainer extends React.Component {
|
|
||||||
static propTypes = {...layerListPropTypes}
|
|
||||||
static defaultProps = {
|
|
||||||
onLayerSelect: () => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
collapsedGroups: {},
|
|
||||||
areAllGroupsExpanded: false,
|
|
||||||
isOpen: {
|
|
||||||
add: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerDestroy(layerId) {
|
|
||||||
const remainingLayers = this.props.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
|
||||||
remainingLayers.splice(idx, 1);
|
|
||||||
this.props.onLayersChange(remainingLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerCopy(layerId) {
|
|
||||||
const changedLayers = this.props.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
|
||||||
|
|
||||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
|
||||||
clonedLayer.id = clonedLayer.id + "-copy"
|
|
||||||
changedLayers.splice(idx, 0, clonedLayer)
|
|
||||||
this.props.onLayersChange(changedLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerVisibilityToggle(layerId) {
|
|
||||||
const changedLayers = this.props.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
|
||||||
|
|
||||||
const layer = { ...changedLayers[idx] }
|
|
||||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
|
||||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
|
||||||
|
|
||||||
layer.layout = changedLayout
|
|
||||||
changedLayers[idx] = layer
|
|
||||||
this.props.onLayersChange(changedLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleModal(modalName) {
|
|
||||||
this.setState({
|
|
||||||
isOpen: {
|
|
||||||
...this.state.isOpen,
|
|
||||||
[modalName]: !this.state.isOpen[modalName]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLayers() {
|
|
||||||
let idx=0
|
|
||||||
|
|
||||||
let newGroups=[]
|
|
||||||
|
|
||||||
this.groupedLayers().forEach(layers => {
|
|
||||||
const groupPrefix = layerPrefix(layers[0].id)
|
|
||||||
const lookupKey = [groupPrefix, idx].join('-')
|
|
||||||
|
|
||||||
|
|
||||||
if (layers.length > 1) {
|
|
||||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded
|
|
||||||
}
|
|
||||||
|
|
||||||
layers.forEach((layer) => {
|
|
||||||
idx += 1
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
collapsedGroups: newGroups,
|
|
||||||
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
groupedLayers() {
|
|
||||||
const groups = []
|
|
||||||
for (let i = 0; i < this.props.layers.length; i++) {
|
|
||||||
const previousLayer = this.props.layers[i-1]
|
|
||||||
const layer = this.props.layers[i]
|
|
||||||
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
|
|
||||||
const lastGroup = groups[groups.length - 1]
|
|
||||||
lastGroup.push(layer)
|
|
||||||
} else {
|
|
||||||
groups.push([layer])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLayerGroup(groupPrefix, idx) {
|
|
||||||
const lookupKey = [groupPrefix, idx].join('-')
|
|
||||||
const newGroups = { ...this.state.collapsedGroups }
|
|
||||||
if(lookupKey in this.state.collapsedGroups) {
|
|
||||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
|
|
||||||
} else {
|
|
||||||
newGroups[lookupKey] = false
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
collapsedGroups: newGroups
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
isCollapsed(groupPrefix, idx) {
|
|
||||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
|
|
||||||
return collapsed === undefined ? true : collapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
const listItems = []
|
|
||||||
let idx = 0
|
|
||||||
this.groupedLayers().forEach(layers => {
|
|
||||||
const groupPrefix = layerPrefix(layers[0].id)
|
|
||||||
if(layers.length > 1) {
|
|
||||||
const grp = <LayerListGroup
|
|
||||||
key={[groupPrefix, idx].join('-')}
|
|
||||||
title={groupPrefix}
|
|
||||||
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
|
||||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
|
||||||
/>
|
|
||||||
listItems.push(grp)
|
|
||||||
}
|
|
||||||
|
|
||||||
layers.forEach((layer, idxInGroup) => {
|
|
||||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
|
||||||
|
|
||||||
const listItem = <LayerListItem
|
|
||||||
className={classnames({
|
|
||||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
|
||||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
|
||||||
})}
|
|
||||||
index={idx}
|
|
||||||
key={layer.id}
|
|
||||||
layerId={layer.id}
|
|
||||||
layerType={layer.type}
|
|
||||||
visibility={(layer.layout || {}).visibility}
|
|
||||||
isSelected={idx === this.props.selectedLayerIndex}
|
|
||||||
onLayerSelect={this.props.onLayerSelect}
|
|
||||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
|
||||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
|
||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
|
||||||
/>
|
|
||||||
listItems.push(listItem)
|
|
||||||
idx += 1
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-layer-list">
|
|
||||||
<AddModal
|
|
||||||
layers={this.props.layers}
|
|
||||||
sources={this.props.sources}
|
|
||||||
isOpen={this.state.isOpen.add}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
|
||||||
onLayersChange={this.props.onLayersChange}
|
|
||||||
/>
|
|
||||||
<header className="maputnik-layer-list-header">
|
|
||||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
|
||||||
<span className="maputnik-space" />
|
|
||||||
<div className="maputnik-default-property">
|
|
||||||
<div className="maputnik-multibutton">
|
|
||||||
<a
|
|
||||||
onClick={this.toggleLayers.bind(this)}
|
|
||||||
className="maputnik-button">
|
|
||||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-default-property">
|
|
||||||
<div className="maputnik-multibutton">
|
|
||||||
<a
|
|
||||||
onClick={this.toggleModal.bind(this, 'add')}
|
|
||||||
className="maputnik-button maputnik-button-selected">
|
|
||||||
Add Layer
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<ul className="maputnik-layer-list-container">
|
|
||||||
{listItems}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LayerList extends React.Component {
|
|
||||||
static propTypes = {...layerListPropTypes}
|
|
||||||
|
|
||||||
onSortEnd(move) {
|
|
||||||
const { oldIndex, newIndex } = move
|
|
||||||
if(oldIndex === newIndex) return
|
|
||||||
let layers = this.props.layers.slice(0)
|
|
||||||
layers = arrayMove(layers, oldIndex, newIndex)
|
|
||||||
this.props.onLayersChange(layers)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <LayerListContainer
|
|
||||||
{...this.props}
|
|
||||||
onSortEnd={this.onSortEnd.bind(this)}
|
|
||||||
useDragHandle={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Collapser from './Collapser'
|
|
||||||
|
|
||||||
export default class LayerListGroup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
isActive: PropTypes.bool.isRequired,
|
|
||||||
onActiveToggle: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <li className="maputnik-layer-list-group">
|
|
||||||
<div className="maputnik-layer-list-group-header"
|
|
||||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
|
||||||
>
|
|
||||||
<span className="maputnik-layer-list-group-title">{this.props.title}</span>
|
|
||||||
<span className="maputnik-space" />
|
|
||||||
<Collapser
|
|
||||||
style={{ height: 14, width: 14 }}
|
|
||||||
isCollapsed={this.props.isActive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Color from 'color'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import CopyIcon from 'react-icons/lib/md/content-copy'
|
|
||||||
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
|
||||||
import VisibilityOffIcon from 'react-icons/lib/md/visibility-off'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
import LayerIcon from '../icons/LayerIcon'
|
|
||||||
import LayerEditor from './LayerEditor'
|
|
||||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
|
||||||
|
|
||||||
@SortableHandle
|
|
||||||
class LayerTypeDragHandle extends React.Component {
|
|
||||||
static propTypes = LayerIcon.propTypes
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <LayerIcon
|
|
||||||
{...this.props}
|
|
||||||
style={{
|
|
||||||
cursor: 'move',
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
paddingRight: 3,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IconAction extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
action: PropTypes.string.isRequired,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon() {
|
|
||||||
switch(this.props.action) {
|
|
||||||
case 'copy': return <CopyIcon />
|
|
||||||
case 'show': return <VisibilityIcon />
|
|
||||||
case 'hide': return <VisibilityOffIcon />
|
|
||||||
case 'delete': return <DeleteIcon />
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
className="maputnik-layer-list-icon-action"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
{this.renderIcon()}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SortableElement
|
|
||||||
class LayerListItem extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
layerId: PropTypes.string.isRequired,
|
|
||||||
layerType: PropTypes.string.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
visibility: PropTypes.string,
|
|
||||||
className: PropTypes.string,
|
|
||||||
|
|
||||||
onLayerSelect: PropTypes.func.isRequired,
|
|
||||||
onLayerCopy: PropTypes.func,
|
|
||||||
onLayerDestroy: PropTypes.func,
|
|
||||||
onLayerVisibilityToggle: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
isSelected: false,
|
|
||||||
visibility: 'visible',
|
|
||||||
onLayerCopy: () => {},
|
|
||||||
onLayerDestroy: () => {},
|
|
||||||
onLayerVisibilityToggle: () => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
static childContextTypes = {
|
|
||||||
reactIconBase: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
reactIconBase: { size: 14 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <li
|
|
||||||
key={this.props.layerId}
|
|
||||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-layer-list-item": true,
|
|
||||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
|
||||||
[this.props.className]: true,
|
|
||||||
})}>
|
|
||||||
<LayerTypeDragHandle type={this.props.layerType} />
|
|
||||||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
|
||||||
<span style={{flexGrow: 1}} />
|
|
||||||
<IconAction
|
|
||||||
action={'delete'}
|
|
||||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
|
||||||
/>
|
|
||||||
<IconAction
|
|
||||||
action={'copy'}
|
|
||||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
|
||||||
/>
|
|
||||||
<IconAction
|
|
||||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
|
||||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerListItem;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
|
||||||
|
|
||||||
class LayerSourceBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
sourceIds: PropTypes.array,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
sourceIds: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}>
|
|
||||||
<AutocompleteInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
options={this.props.sourceIds.map(src => [src, src])}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerSourceBlock
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
|
||||||
|
|
||||||
class LayerSourceLayer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
sourceLayerIds: PropTypes.array,
|
|
||||||
isFixed: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
sourceLayerIds: [],
|
|
||||||
isFixed: false
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}>
|
|
||||||
<AutocompleteInput
|
|
||||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerSourceLayer
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
|
|
||||||
class LayerTypeBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}>
|
|
||||||
<SelectInput
|
|
||||||
options={[
|
|
||||||
['background', 'Background'],
|
|
||||||
['fill', 'Fill'],
|
|
||||||
['line', 'Line'],
|
|
||||||
['symbol', 'Symbol'],
|
|
||||||
['raster', 'Raster'],
|
|
||||||
['circle', 'Circle'],
|
|
||||||
['fill-extrusion', 'Fill Extrusion'],
|
|
||||||
]}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
value={this.props.value}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerTypeBlock
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
|
|
||||||
class MaxZoomBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.number,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}>
|
|
||||||
<NumberInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
min={styleSpec.latest.layer.maxzoom.minimum}
|
|
||||||
max={styleSpec.latest.layer.maxzoom.maximum}
|
|
||||||
default={styleSpec.latest.layer.maxzoom.maximum}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MaxZoomBlock
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
|
|
||||||
class MinZoomBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.number,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}>
|
|
||||||
<NumberInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
min={styleSpec.latest.layer.minzoom.minimum}
|
|
||||||
max={styleSpec.latest.layer.minzoom.maximum}
|
|
||||||
default={styleSpec.latest.layer.minzoom.minimum}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MinZoomBlock
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import LayerIcon from '../icons/LayerIcon'
|
|
||||||
|
|
||||||
function groupFeaturesBySourceLayer(features) {
|
|
||||||
const sources = {}
|
|
||||||
|
|
||||||
let returnedFeatures = {};
|
|
||||||
|
|
||||||
features.forEach(feature => {
|
|
||||||
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
|
|
||||||
returnedFeatures[feature.layer.id]++
|
|
||||||
|
|
||||||
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
|
|
||||||
|
|
||||||
featureObject.counter = returnedFeatures[feature.layer.id]
|
|
||||||
} else {
|
|
||||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
|
||||||
sources[feature.layer['source-layer']].push(feature)
|
|
||||||
|
|
||||||
returnedFeatures[feature.layer.id] = 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeatureLayerPopup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onLayerSelect: PropTypes.func.isRequired,
|
|
||||||
features: PropTypes.array
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
|
||||||
|
|
||||||
const items = Object.keys(sources).map(vectorLayerId => {
|
|
||||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
|
||||||
return <label
|
|
||||||
key={idx}
|
|
||||||
className="maputnik-popup-layer"
|
|
||||||
onClick={() => {
|
|
||||||
this.props.onLayerSelect(feature.layer.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayerIcon type={feature.layer.type} style={{
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
paddingRight: 3
|
|
||||||
}}/>
|
|
||||||
{feature.layer.id}
|
|
||||||
{feature.counter && <span> × {feature.counter}</span>}
|
|
||||||
</label>
|
|
||||||
})
|
|
||||||
return <div key={vectorLayerId}>
|
|
||||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
|
||||||
{layers}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-feature-layer-popup">
|
|
||||||
{items}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default FeatureLayerPopup
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
|
|
||||||
function displayValue(value) {
|
|
||||||
if (typeof value === 'undefined' || value === null) return value;
|
|
||||||
if (value instanceof Date) return value.toLocaleString();
|
|
||||||
if (typeof value === 'object' ||
|
|
||||||
typeof value === 'number' ||
|
|
||||||
typeof value === 'string') return value.toString();
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderProperties(feature) {
|
|
||||||
return Object.keys(feature.properties).map(propertyName => {
|
|
||||||
const property = feature.properties[propertyName]
|
|
||||||
return <InputBlock key={propertyName} label={propertyName}>
|
|
||||||
<StringInput value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
|
||||||
</InputBlock>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFeature(feature) {
|
|
||||||
return <div key={feature.id}>
|
|
||||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
|
||||||
<InputBlock key={"property-type"} label={"$type"}>
|
|
||||||
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
|
||||||
</InputBlock>
|
|
||||||
{renderProperties(feature)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDuplicatedFeatures(features) {
|
|
||||||
let uniqueFeatures = [];
|
|
||||||
|
|
||||||
features.forEach(feature => {
|
|
||||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
|
||||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
|
||||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
|
||||||
})
|
|
||||||
|
|
||||||
if(featureIndex === -1) {
|
|
||||||
uniqueFeatures.push(feature)
|
|
||||||
} else {
|
|
||||||
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
|
|
||||||
uniqueFeatures[featureIndex].inspectModeCounter++
|
|
||||||
} else {
|
|
||||||
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return uniqueFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeaturePropertyPopup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
features: PropTypes.array
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const features = removeDuplicatedFeatures(this.props.features)
|
|
||||||
return <div className="maputnik-feature-property-popup">
|
|
||||||
{features.map(renderFeature)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default FeaturePropertyPopup
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import MapboxGl from 'mapbox-gl'
|
|
||||||
import MapboxInspect from 'mapbox-gl-inspect'
|
|
||||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
|
||||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
|
||||||
import style from '../../libs/style.js'
|
|
||||||
import tokens from '../../config/tokens.json'
|
|
||||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
|
||||||
import Color from 'color'
|
|
||||||
import ZoomControl from '../../libs/zoomcontrol'
|
|
||||||
import { colorHighlightedLayer } from '../../libs/highlight'
|
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
|
||||||
import '../../mapboxgl.css'
|
|
||||||
import '../../libs/mapbox-rtl'
|
|
||||||
|
|
||||||
function renderPropertyPopup(features) {
|
|
||||||
var mountNode = document.createElement('div');
|
|
||||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
|
||||||
return mountNode.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
|
||||||
const backgroundLayer = {
|
|
||||||
"id": "background",
|
|
||||||
"type": "background",
|
|
||||||
"paint": {
|
|
||||||
"background-color": '#1c1f24',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const layer = colorHighlightedLayer(highlightedLayer)
|
|
||||||
if(layer) {
|
|
||||||
coloredLayers.push(layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sources = {}
|
|
||||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
|
||||||
const source = originalMapStyle.sources[sourceId]
|
|
||||||
if(source.type !== 'raster') {
|
|
||||||
sources[sourceId] = source
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const inspectStyle = {
|
|
||||||
...originalMapStyle,
|
|
||||||
sources: sources,
|
|
||||||
layers: [backgroundLayer].concat(coloredLayers)
|
|
||||||
}
|
|
||||||
return inspectStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MapboxGlMap extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onDataChange: PropTypes.func,
|
|
||||||
onLayerSelect: PropTypes.func.isRequired,
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
|
||||||
highlightedLayer: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onMapLoaded: () => {},
|
|
||||||
onDataChange: () => {},
|
|
||||||
onLayerSelect: () => {},
|
|
||||||
mapboxAccessToken: tokens.mapbox,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
MapboxGl.accessToken = tokens.mapbox
|
|
||||||
this.state = {
|
|
||||||
map: null,
|
|
||||||
inspect: null,
|
|
||||||
isPopupOpen: false,
|
|
||||||
popupX: 0,
|
|
||||||
popupY: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if(!this.state.map) return
|
|
||||||
const metadata = nextProps.mapStyle.metadata || {}
|
|
||||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
|
||||||
|
|
||||||
if(!nextProps.inspectModeEnabled) {
|
|
||||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
|
||||||
//the necessary operations ourselves!
|
|
||||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
|
||||||
this.state.inspect.toggleInspector()
|
|
||||||
}
|
|
||||||
if(this.props.inspectModeEnabled) {
|
|
||||||
this.state.inspect.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const map = new MapboxGl.Map({
|
|
||||||
container: this.container,
|
|
||||||
style: this.props.mapStyle,
|
|
||||||
hash: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const zoom = new ZoomControl;
|
|
||||||
map.addControl(zoom, 'top-right');
|
|
||||||
|
|
||||||
const nav = new MapboxGl.NavigationControl();
|
|
||||||
map.addControl(nav, 'top-right');
|
|
||||||
|
|
||||||
const inspect = new MapboxInspect({
|
|
||||||
popup: new MapboxGl.Popup({
|
|
||||||
closeOnClick: false
|
|
||||||
}),
|
|
||||||
showMapPopup: true,
|
|
||||||
showMapPopupOnHover: false,
|
|
||||||
showInspectMapPopupOnHover: true,
|
|
||||||
showInspectButton: false,
|
|
||||||
assignLayerColor: (layerId, alpha) => {
|
|
||||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
|
||||||
},
|
|
||||||
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
|
||||||
renderPopup: features => {
|
|
||||||
if(this.props.inspectModeEnabled) {
|
|
||||||
return renderPropertyPopup(features)
|
|
||||||
} else {
|
|
||||||
var mountNode = document.createElement('div');
|
|
||||||
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
|
|
||||||
return mountNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
map.addControl(inspect)
|
|
||||||
|
|
||||||
map.on("style.load", () => {
|
|
||||||
this.setState({ map, inspect });
|
|
||||||
})
|
|
||||||
|
|
||||||
map.on("data", e => {
|
|
||||||
if(e.dataType !== 'tile') return
|
|
||||||
this.props.onDataChange({
|
|
||||||
map: this.state.map
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
className="maputnik-map"
|
|
||||||
ref={x => this.container = x}
|
|
||||||
></div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import style from '../../libs/style.js'
|
|
||||||
import isEqual from 'lodash.isequal'
|
|
||||||
import { loadJSON } from '../../libs/urlopen'
|
|
||||||
import 'openlayers/dist/ol.css'
|
|
||||||
|
|
||||||
function suitableVectorSource(mapStyle) {
|
|
||||||
const sources = Object.keys(mapStyle.sources)
|
|
||||||
.map(sourceId => {
|
|
||||||
return {
|
|
||||||
id: sourceId,
|
|
||||||
source: mapStyle.sources[sourceId]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(({source}) => (source.type === 'vector' || source.type === 'geojson'))
|
|
||||||
return sources[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function toVectorLayer(source, tilegrid, cb) {
|
|
||||||
function newMVTLayer(tileUrl) {
|
|
||||||
const ol = require('openlayers')
|
|
||||||
return new ol.layer.VectorTile({
|
|
||||||
source: new ol.source.VectorTile({
|
|
||||||
format: new ol.format.MVT(),
|
|
||||||
tileGrid: tilegrid,
|
|
||||||
tilePixelRatio: 8,
|
|
||||||
url: tileUrl
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function newGeoJSONLayer(sourceUrl) {
|
|
||||||
const ol = require('openlayers')
|
|
||||||
return new ol.layer.Vector({
|
|
||||||
source: new ol.source.Vector({
|
|
||||||
format: new ol.format.GeoJSON(),
|
|
||||||
url: sourceUrl
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (source.type === 'vector') {
|
|
||||||
if(!source.tiles) {
|
|
||||||
sourceFromTileJSON(source.url, tileSource => {
|
|
||||||
cb(newMVTLayer(tileSource.tiles[0]))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
cb(newMVTLayer(source.tiles[0]))
|
|
||||||
}
|
|
||||||
} else if (source.type === 'geojson') {
|
|
||||||
cb(newGeoJSONLayer(source.data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sourceFromTileJSON(url, cb) {
|
|
||||||
loadJSON(url, null, tilejson => {
|
|
||||||
if(!tilejson) return
|
|
||||||
cb({
|
|
||||||
type: 'vector',
|
|
||||||
tiles: tilejson.tiles,
|
|
||||||
minzoom: tilejson.minzoom,
|
|
||||||
maxzoom: tilejson.maxzoom,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenLayers3Map extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onDataChange: PropTypes.func,
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
accessToken: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onMapLoaded: () => {},
|
|
||||||
onDataChange: () => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.tilegrid = null
|
|
||||||
this.resolutions = null
|
|
||||||
this.layer = null
|
|
||||||
this.map = null
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStyle(newMapStyle) {
|
|
||||||
const oldSource = suitableVectorSource(this.props.mapStyle)
|
|
||||||
const newSource = suitableVectorSource(newMapStyle)
|
|
||||||
const resolutions = this.resolutions
|
|
||||||
|
|
||||||
function setStyleFunc(map, layer) {
|
|
||||||
const olms = require('ol-mapbox-style')
|
|
||||||
const styleFunc = olms.getStyleFunction(newMapStyle, newSource.id, resolutions)
|
|
||||||
layer.setStyle(styleFunc)
|
|
||||||
//NOTE: We need to mark the source as changed in order
|
|
||||||
//to trigger a rerender
|
|
||||||
layer.getSource().changed()
|
|
||||||
map.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
if(newSource) {
|
|
||||||
if(this.layer && !isEqual(oldSource, newSource)) {
|
|
||||||
this.map.removeLayer(this.layer)
|
|
||||||
this.layer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.layer) {
|
|
||||||
var self = this
|
|
||||||
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
|
|
||||||
self.layer = vectorLayer
|
|
||||||
self.map.addLayer(self.layer)
|
|
||||||
setStyleFunc(self.map, self.layer)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setStyleFunc(this.map, this.layer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
require.ensure(["openlayers", "ol-mapbox-style"], () => {
|
|
||||||
if(!this.map || !this.resolutions) return
|
|
||||||
this.updateStyle(nextProps.mapStyle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
//Load OpenLayers dynamically once we need it
|
|
||||||
//TODO: Make this more convenient
|
|
||||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
|
||||||
console.log('Loaded OpenLayers3 renderer')
|
|
||||||
|
|
||||||
const ol = require('openlayers')
|
|
||||||
const olms = require('ol-mapbox-style')
|
|
||||||
|
|
||||||
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
|
||||||
this.resolutions = this.tilegrid.getResolutions()
|
|
||||||
|
|
||||||
const map = new ol.Map({
|
|
||||||
target: this.container,
|
|
||||||
layers: [],
|
|
||||||
view: new ol.View({
|
|
||||||
zoom: 2,
|
|
||||||
center: [52.5, -78.4]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
map.addControl(new ol.control.Zoom())
|
|
||||||
this.map = map
|
|
||||||
this.updateStyle(this.props.mapStyle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
ref={x => this.container = x}
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: 40,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
height: 'calc(100% - 40px)',
|
|
||||||
width: "75%",
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
...this.props.style,
|
|
||||||
}}>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OpenLayers3Map
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import Button from '../Button'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import Modal from './Modal'
|
|
||||||
|
|
||||||
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
|
||||||
import LayerIdBlock from '../layers/LayerIdBlock'
|
|
||||||
import LayerSourceBlock from '../layers/LayerSourceBlock'
|
|
||||||
import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
|
||||||
|
|
||||||
class AddModal extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
layers: PropTypes.array.isRequired,
|
|
||||||
onLayersChange: PropTypes.func.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpenToggle: PropTypes.func.isRequired,
|
|
||||||
|
|
||||||
// A dict of source id's and the available source layers
|
|
||||||
sources: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
addLayer() {
|
|
||||||
const changedLayers = this.props.layers.slice(0)
|
|
||||||
const layer = {
|
|
||||||
id: this.state.id,
|
|
||||||
type: this.state.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.type !== 'background') {
|
|
||||||
layer.source = this.state.source
|
|
||||||
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
|
||||||
layer['source-layer'] = this.state['source-layer']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changedLayers.push(layer)
|
|
||||||
|
|
||||||
this.props.onLayersChange(changedLayers)
|
|
||||||
this.props.onOpenToggle(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
type: 'fill',
|
|
||||||
id: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
if(props.sources.length > 0) {
|
|
||||||
this.state.source = Object.keys(this.props.sources)[0]
|
|
||||||
this.state['source-layer'] = this.props.sources[this.state.source][0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUpdate(nextProps, nextState) {
|
|
||||||
// Check if source is valid for new type
|
|
||||||
const oldType = this.state.type;
|
|
||||||
const newType = nextState.type;
|
|
||||||
|
|
||||||
const availableSourcesOld = this.getSources(oldType);
|
|
||||||
const availableSourcesNew = this.getSources(newType);
|
|
||||||
|
|
||||||
if(
|
|
||||||
// Type has changed
|
|
||||||
oldType !== newType
|
|
||||||
&& this.state.source !== ""
|
|
||||||
// Was a valid source previously
|
|
||||||
&& availableSourcesOld.indexOf(this.state.source) > -1
|
|
||||||
// And is not a valid source now
|
|
||||||
&& availableSourcesNew.indexOf(nextState.source) < 0
|
|
||||||
) {
|
|
||||||
// Clear the source
|
|
||||||
this.setState({
|
|
||||||
source: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLayersForSource(source) {
|
|
||||||
const sourceObj = this.props.sources[source] || {};
|
|
||||||
return sourceObj.layers || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getSources(type) {
|
|
||||||
const sources = [];
|
|
||||||
|
|
||||||
const types = {
|
|
||||||
vector: [
|
|
||||||
"fill",
|
|
||||||
"line",
|
|
||||||
"symbol",
|
|
||||||
"circle",
|
|
||||||
"fill-extrusion"
|
|
||||||
],
|
|
||||||
raster: [
|
|
||||||
"raster"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let [key, val] of Object.entries(this.props.sources)) {
|
|
||||||
if(types[val.type] && types[val.type].indexOf(type) > -1) {
|
|
||||||
sources.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sources;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sources = this.getSources(this.state.type);
|
|
||||||
const layers = this.getLayersForSource(this.state.source);
|
|
||||||
|
|
||||||
return <Modal
|
|
||||||
isOpen={this.props.isOpen}
|
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
|
||||||
title={'Add Layer'}
|
|
||||||
>
|
|
||||||
<div className="maputnik-add-layer">
|
|
||||||
<LayerIdBlock
|
|
||||||
value={this.state.id}
|
|
||||||
onChange={v => this.setState({ id: v })}
|
|
||||||
/>
|
|
||||||
<LayerTypeBlock
|
|
||||||
value={this.state.type}
|
|
||||||
onChange={v => this.setState({ type: v })}
|
|
||||||
/>
|
|
||||||
{this.state.type !== 'background' &&
|
|
||||||
<LayerSourceBlock
|
|
||||||
sourceIds={sources}
|
|
||||||
value={this.state.source}
|
|
||||||
onChange={v => this.setState({ source: v })}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{this.state.type !== 'background' && this.state.type !== 'raster' &&
|
|
||||||
<LayerSourceLayerBlock
|
|
||||||
isFixed={true}
|
|
||||||
sourceLayerIds={layers}
|
|
||||||
value={this.state['source-layer']}
|
|
||||||
onChange={v => this.setState({ 'source-layer': v })}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<Button className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}>
|
|
||||||
Add Layer
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddModal
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { saveAs } from 'file-saver'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import CheckboxInput from '../inputs/CheckboxInput'
|
|
||||||
import Button from '../Button'
|
|
||||||
import Modal from './Modal'
|
|
||||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
|
||||||
import TiClipboard from 'react-icons/lib/ti/clipboard'
|
|
||||||
import style from '../../libs/style.js'
|
|
||||||
import GitHub from 'github-api'
|
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
|
||||||
|
|
||||||
|
|
||||||
class Gist extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
onStyleChanged: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
preview: false,
|
|
||||||
saving: false,
|
|
||||||
latestGist: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onSave() {
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
saving: true
|
|
||||||
});
|
|
||||||
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
|
|
||||||
|
|
||||||
const mapStyleStr = preview ?
|
|
||||||
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
|
||||||
styleSpec.format(stripAccessTokens(this.props.mapStyle));
|
|
||||||
const styleTitle = this.props.mapStyle.name || 'Style';
|
|
||||||
const htmlStr = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>`+styleTitle+` Preview</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.css" />
|
|
||||||
<script src="https://api.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.js"></script>
|
|
||||||
<style>
|
|
||||||
body { margin:0; padding:0; }
|
|
||||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id='map'></div>
|
|
||||||
<script>
|
|
||||||
var map = new mapboxgl.Map({
|
|
||||||
container: 'map',
|
|
||||||
style: 'style.json',
|
|
||||||
attributionControl: true,
|
|
||||||
hash: true
|
|
||||||
});
|
|
||||||
map.addControl(new mapboxgl.NavigationControl());
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
const files = {
|
|
||||||
"style.json": {
|
|
||||||
content: mapStyleStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(preview) {
|
|
||||||
files["index.html"] = {
|
|
||||||
content: htmlStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const gh = new GitHub();
|
|
||||||
let gist = gh.getGist(); // not a gist yet
|
|
||||||
gist.create({
|
|
||||||
public: true,
|
|
||||||
description: styleTitle,
|
|
||||||
files: files
|
|
||||||
}).then(function({data}) {
|
|
||||||
return gist.read();
|
|
||||||
}).then(function({data}) {
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
latestGist: data,
|
|
||||||
saving: false,
|
|
||||||
});
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreviewChange(value) {
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
preview: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMetadataProperty(property, value) {
|
|
||||||
const changedStyle = {
|
|
||||||
...this.props.mapStyle,
|
|
||||||
metadata: {
|
|
||||||
...this.props.mapStyle.metadata,
|
|
||||||
[property]: value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.props.onStyleChanged(changedStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPreviewLink() {
|
|
||||||
const gist = this.state.latestGist;
|
|
||||||
const user = gist.user || 'anonymous';
|
|
||||||
const preview = !!gist.files['index.html'];
|
|
||||||
if(preview) {
|
|
||||||
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLatestGist() {
|
|
||||||
const gist = this.state.latestGist;
|
|
||||||
const saving = this.state.saving;
|
|
||||||
if(saving) {
|
|
||||||
return <p>Saving...</p>
|
|
||||||
} else if(gist) {
|
|
||||||
const user = gist.user || 'anonymous';
|
|
||||||
const rawGistLink = "https://gist.githubusercontent.com/" + user + "/" + gist.id + "/raw/" + gist.history[0].version + "/style.json"
|
|
||||||
const maputnikStyleLink = "https://maputnik.github.io/editor/?style=" + rawGistLink
|
|
||||||
return <div className="maputnik-render-gist">
|
|
||||||
<p>
|
|
||||||
Latest saved gist:{' '}
|
|
||||||
{this.renderPreviewLink(this)}
|
|
||||||
<a target="_blank" rel="noopener noreferrer" href={"https://gist.github.com/" + user + "/" + gist.id}>Source</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<CopyToClipboard text={maputnikStyleLink}>
|
|
||||||
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
|
|
||||||
</CopyToClipboard>
|
|
||||||
<StringInput value={maputnikStyleLink} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-export-gist">
|
|
||||||
<Button onClick={this.onSave.bind(this)}>
|
|
||||||
<MdFileDownload />
|
|
||||||
Save to Gist (anonymous)
|
|
||||||
</Button>
|
|
||||||
{' '}
|
|
||||||
<CheckboxInput
|
|
||||||
value={this.state.preview}
|
|
||||||
name='gist-style-preview'
|
|
||||||
onChange={this.onPreviewChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span> Include preview</span>
|
|
||||||
{this.state.preview ?
|
|
||||||
<div>
|
|
||||||
<InputBlock
|
|
||||||
label={"OpenMapTiles Access Token: "}>
|
|
||||||
<StringInput
|
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
|
||||||
</InputBlock>
|
|
||||||
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
|
||||||
</div>
|
|
||||||
: null}
|
|
||||||
{this.renderLatestGist()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripAccessTokens(mapStyle) {
|
|
||||||
const changedMetadata = { ...mapStyle.metadata }
|
|
||||||
delete changedMetadata['maputnik:mapbox_access_token']
|
|
||||||
delete changedMetadata['maputnik:openmaptiles_access_token']
|
|
||||||
return {
|
|
||||||
...mapStyle,
|
|
||||||
metadata: changedMetadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExportModal extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
onStyleChanged: PropTypes.func.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpenToggle: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadStyle() {
|
|
||||||
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
|
|
||||||
saveAs(blob, this.props.mapStyle.id + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Modal
|
|
||||||
isOpen={this.props.isOpen}
|
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
|
||||||
title={'Export Style'}
|
|
||||||
>
|
|
||||||
|
|
||||||
<div className="maputnik-modal-section">
|
|
||||||
<h4>Download Style</h4>
|
|
||||||
<p>
|
|
||||||
Download a JSON style to your computer.
|
|
||||||
</p>
|
|
||||||
<Button onClick={this.downloadStyle.bind(this)}>
|
|
||||||
<MdFileDownload />
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="maputnik-modal-section">
|
|
||||||
<h4>Save style</h4>
|
|
||||||
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExportModal
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import CloseIcon from 'react-icons/lib/md/close'
|
|
||||||
import Overlay from './Overlay'
|
|
||||||
|
|
||||||
class Modal extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
onOpenToggle: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Overlay isOpen={this.props.isOpen}>
|
|
||||||
<div className="maputnik-modal">
|
|
||||||
<header className="maputnik-modal-header">
|
|
||||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
|
||||||
<span className="maputnik-modal-header-space"></span>
|
|
||||||
<a className="maputnik-modal-header-toggle"
|
|
||||||
onClick={() => this.props.onOpenToggle(false)}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
<div className="maputnik-modal-scroller">
|
|
||||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Overlay>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Modal
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Modal from './Modal'
|
|
||||||
import Button from '../Button'
|
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
|
||||||
import request from 'request'
|
|
||||||
|
|
||||||
import FileUploadIcon from 'react-icons/lib/md/file-upload'
|
|
||||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
|
||||||
|
|
||||||
import style from '../../libs/style.js'
|
|
||||||
import publicStyles from '../../config/styles.json'
|
|
||||||
|
|
||||||
class PublicStyle extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
thumbnailUrl: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-public-style">
|
|
||||||
<Button
|
|
||||||
className="maputnik-public-style-button"
|
|
||||||
onClick={() => this.props.onSelect(this.props.url)}
|
|
||||||
>
|
|
||||||
<header className="maputnik-public-style-header">
|
|
||||||
<h4>{this.props.title}</h4>
|
|
||||||
<span className="maputnik-space" />
|
|
||||||
<AddIcon />
|
|
||||||
</header>
|
|
||||||
<img
|
|
||||||
className="maputnik-public-style-thumbnail"
|
|
||||||
src={this.props.thumbnailUrl}
|
|
||||||
alt={this.props.title}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenModal extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpenToggle: PropTypes.func.isRequired,
|
|
||||||
onStyleOpen: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
clearError() {
|
|
||||||
this.setState({
|
|
||||||
error: null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onStyleSelect(styleUrl) {
|
|
||||||
this.clearError();
|
|
||||||
|
|
||||||
request({
|
|
||||||
url: styleUrl,
|
|
||||||
withCredentials: false,
|
|
||||||
}, (error, response, body) => {
|
|
||||||
if (!error && response.statusCode == 200) {
|
|
||||||
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
|
||||||
console.log('Loaded style ', mapStyle.id)
|
|
||||||
this.props.onStyleOpen(mapStyle)
|
|
||||||
this.onOpenToggle()
|
|
||||||
} else {
|
|
||||||
console.warn('Could not open the style URL', styleUrl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenUrl() {
|
|
||||||
const url = this.styleUrlElement.value;
|
|
||||||
this.onStyleSelect(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpload(_, files) {
|
|
||||||
const [e, file] = files[0];
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
this.clearError();
|
|
||||||
|
|
||||||
reader.readAsText(file, "UTF-8");
|
|
||||||
reader.onload = e => {
|
|
||||||
let mapStyle;
|
|
||||||
try {
|
|
||||||
mapStyle = JSON.parse(e.target.result)
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
this.setState({
|
|
||||||
error: err.toString()
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
|
||||||
this.props.onStyleOpen(mapStyle);
|
|
||||||
this.onOpenToggle();
|
|
||||||
}
|
|
||||||
reader.onerror = e => console.log(e.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenToggle() {
|
|
||||||
this.clearError();
|
|
||||||
this.props.onOpenToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const styleOptions = publicStyles.map(style => {
|
|
||||||
return <PublicStyle
|
|
||||||
key={style.id}
|
|
||||||
url={style.url}
|
|
||||||
title={style.title}
|
|
||||||
thumbnailUrl={style.thumbnail}
|
|
||||||
onSelect={this.onStyleSelect.bind(this)}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
let errorElement;
|
|
||||||
if(this.state.error) {
|
|
||||||
errorElement = (
|
|
||||||
<div className="maputnik-modal-error">
|
|
||||||
{this.state.error}
|
|
||||||
<a href="#" onClick={() => this.clearError()} className="maputnik-modal-error-close">×</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Modal
|
|
||||||
isOpen={this.props.isOpen}
|
|
||||||
onOpenToggle={() => this.onOpenToggle()}
|
|
||||||
title={'Open Style'}
|
|
||||||
>
|
|
||||||
{errorElement}
|
|
||||||
<section className="maputnik-modal-section">
|
|
||||||
<h2>Upload Style</h2>
|
|
||||||
<p>Upload a JSON style from your computer.</p>
|
|
||||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
|
||||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
|
||||||
</FileReaderInput>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="maputnik-modal-section">
|
|
||||||
<h2>Load from URL</h2>
|
|
||||||
<p>
|
|
||||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
|
||||||
</p>
|
|
||||||
<input type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
|
|
||||||
<div>
|
|
||||||
<Button className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
|
||||||
<h2>Gallery Styles</h2>
|
|
||||||
<p>
|
|
||||||
Open one of the publicly available styles to start from.
|
|
||||||
</p>
|
|
||||||
<div className="maputnik-style-gallery-container">
|
|
||||||
{styleOptions}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OpenModal
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
|
|
||||||
class Overlay extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.element.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let overlayStyle = {}
|
|
||||||
if(!this.props.isOpen) {
|
|
||||||
overlayStyle['display'] = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={"maputnik-overlay"} style={overlayStyle}>
|
|
||||||
<div className={"maputnik-overlay-viewport"} />
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Overlay
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import Modal from './Modal'
|
|
||||||
|
|
||||||
class SettingsModal extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
onStyleChanged: PropTypes.func.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpenToggle: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeStyleProperty(property, value) {
|
|
||||||
const changedStyle = {
|
|
||||||
...this.props.mapStyle,
|
|
||||||
[property]: value
|
|
||||||
}
|
|
||||||
this.props.onStyleChanged(changedStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMetadataProperty(property, value) {
|
|
||||||
const changedStyle = {
|
|
||||||
...this.props.mapStyle,
|
|
||||||
metadata: {
|
|
||||||
...this.props.mapStyle.metadata,
|
|
||||||
[property]: value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.props.onStyleChanged(changedStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const metadata = this.props.mapStyle.metadata || {}
|
|
||||||
const inputProps = { }
|
|
||||||
return <Modal
|
|
||||||
isOpen={this.props.isOpen}
|
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
|
||||||
title={'Style Settings'}
|
|
||||||
>
|
|
||||||
<div style={{minWidth: 350}}>
|
|
||||||
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
|
|
||||||
<StringInput {...inputProps}
|
|
||||||
value={this.props.mapStyle.name}
|
|
||||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
|
||||||
<StringInput {...inputProps}
|
|
||||||
value={this.props.mapStyle.owner}
|
|
||||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
|
|
||||||
<StringInput {...inputProps}
|
|
||||||
value={this.props.mapStyle.sprite}
|
|
||||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
|
|
||||||
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
|
|
||||||
<StringInput {...inputProps}
|
|
||||||
value={this.props.mapStyle.glyphs}
|
|
||||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
|
|
||||||
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
|
||||||
<StringInput {...inputProps}
|
|
||||||
value={metadata['maputnik:mapbox_access_token']}
|
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
|
|
||||||
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
|
|
||||||
<StringInput {...inputProps}
|
|
||||||
value={metadata['maputnik:openmaptiles_access_token']}
|
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
|
|
||||||
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
|
|
||||||
<SelectInput {...inputProps}
|
|
||||||
options={[
|
|
||||||
['mbgljs', 'MapboxGL JS'],
|
|
||||||
['ol3', 'Open Layers 3'],
|
|
||||||
]}
|
|
||||||
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
|
||||||
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SettingsModal
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import Modal from './Modal'
|
|
||||||
import Button from '../Button'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import SourceTypeEditor from '../sources/SourceTypeEditor'
|
|
||||||
|
|
||||||
import style from '../../libs/style'
|
|
||||||
import { deleteSource, addSource, changeSource } from '../../libs/source'
|
|
||||||
import publicSources from '../../config/tilesets.json'
|
|
||||||
|
|
||||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
class PublicSource extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
onSelect: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-public-source">
|
|
||||||
<Button
|
|
||||||
className="maputnik-public-source-select"
|
|
||||||
onClick={() => this.props.onSelect(this.props.id)}
|
|
||||||
>
|
|
||||||
<div className="maputnik-public-source-info">
|
|
||||||
<p className="maputnik-public-source-name">{this.props.title}</p>
|
|
||||||
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
|
||||||
</div>
|
|
||||||
<span className="maputnik-space" />
|
|
||||||
<AddIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function editorMode(source) {
|
|
||||||
if(source.type === 'raster') {
|
|
||||||
if(source.tiles) return 'tilexyz_raster'
|
|
||||||
return 'tilejson_raster'
|
|
||||||
}
|
|
||||||
if(source.type === 'vector') {
|
|
||||||
if(source.tiles) return 'tilexyz_vector'
|
|
||||||
return 'tilejson_vector'
|
|
||||||
}
|
|
||||||
if(source.type === 'geojson') return 'geojson'
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActiveSourceTypeEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
sourceId: PropTypes.string.isRequired,
|
|
||||||
source: PropTypes.object.isRequired,
|
|
||||||
onDelete: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputProps = { }
|
|
||||||
return <div className="maputnik-active-source-type-editor">
|
|
||||||
<div className="maputnik-active-source-type-editor-header">
|
|
||||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
|
||||||
<span className="maputnik-space" />
|
|
||||||
<Button
|
|
||||||
className="maputnik-active-source-type-editor-header-delete"
|
|
||||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
|
||||||
style={{backgroundColor: 'transparent'}}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-active-source-type-editor-content">
|
|
||||||
<SourceTypeEditor
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
mode={editorMode(this.props.source)}
|
|
||||||
source={this.props.source}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddSource extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onAdd: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
mode: 'tilejson_vector',
|
|
||||||
sourceId: style.generateId(),
|
|
||||||
source: this.defaultSource('tilejson_vector'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultSource(mode) {
|
|
||||||
const source = (this.state || {}).source || {}
|
|
||||||
switch(mode) {
|
|
||||||
case 'geojson': return {
|
|
||||||
type: 'geojson',
|
|
||||||
data: source.data || 'http://localhost:3000/geojson.json'
|
|
||||||
}
|
|
||||||
case 'tilejson_vector': return {
|
|
||||||
type: 'vector',
|
|
||||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
|
||||||
}
|
|
||||||
case 'tilexyz_vector': return {
|
|
||||||
type: 'vector',
|
|
||||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
|
||||||
minZoom: source.minzoom || 0,
|
|
||||||
maxZoom: source.maxzoom || 14
|
|
||||||
}
|
|
||||||
case 'tilejson_raster': return {
|
|
||||||
type: 'raster',
|
|
||||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
|
||||||
}
|
|
||||||
case 'tilexyz_raster': return {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
|
||||||
minzoom: source.minzoom || 0,
|
|
||||||
maxzoom: source.maxzoom || 14
|
|
||||||
}
|
|
||||||
default: return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-add-source">
|
|
||||||
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
|
|
||||||
<StringInput
|
|
||||||
value={this.state.sourceId}
|
|
||||||
onChange={v => this.setState({ sourceId: v})}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
|
|
||||||
<SelectInput
|
|
||||||
options={[
|
|
||||||
['geojson', 'GeoJSON'],
|
|
||||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
|
||||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
|
||||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
|
||||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
|
||||||
]}
|
|
||||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
|
||||||
value={this.state.mode}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
<SourceTypeEditor
|
|
||||||
onChange={src => this.setState({ source: src })}
|
|
||||||
mode={this.state.mode}
|
|
||||||
source={this.state.source}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-source-button"
|
|
||||||
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
|
|
||||||
Add Source
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SourcesModal extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpenToggle: PropTypes.func.isRequired,
|
|
||||||
onStyleChanged: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
stripTitle(source) {
|
|
||||||
const strippedSource = {...source}
|
|
||||||
delete strippedSource['title']
|
|
||||||
return strippedSource
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const mapStyle = this.props.mapStyle
|
|
||||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
|
||||||
const source = mapStyle.sources[sourceId]
|
|
||||||
return <ActiveSourceTypeEditor
|
|
||||||
key={sourceId}
|
|
||||||
sourceId={sourceId}
|
|
||||||
source={source}
|
|
||||||
onChange={src => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
|
||||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in mapStyle.sources)).map(sourceId => {
|
|
||||||
const source = publicSources[sourceId]
|
|
||||||
return <PublicSource
|
|
||||||
key={sourceId}
|
|
||||||
id={sourceId}
|
|
||||||
type={source.type}
|
|
||||||
title={source.title}
|
|
||||||
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
const inputProps = { }
|
|
||||||
return <Modal
|
|
||||||
isOpen={this.props.isOpen}
|
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
|
||||||
title={'Sources'}
|
|
||||||
>
|
|
||||||
<div className="maputnik-modal-section">
|
|
||||||
<h4>Active Sources</h4>
|
|
||||||
{activeSources}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="maputnik-modal-section">
|
|
||||||
<h4>Choose Public Source</h4>
|
|
||||||
<p>
|
|
||||||
Add one of the publicly availble sources to your style.
|
|
||||||
</p>
|
|
||||||
<div style={{maxwidth: 500}}>
|
|
||||||
{tilesetOptions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="maputnik-modal-section">
|
|
||||||
<h4>Add New Source</h4>
|
|
||||||
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
|
||||||
<AddSource
|
|
||||||
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SourcesModal
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
|
|
||||||
class TileJSONSourceEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
source: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
|
|
||||||
<StringInput
|
|
||||||
value={this.props.source.url}
|
|
||||||
onChange={url => this.props.onChange({
|
|
||||||
...this.props.source,
|
|
||||||
url: url
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TileURLSourceEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
source: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTileUrl(idx, value) {
|
|
||||||
const tiles = this.props.source.tiles.slice(0)
|
|
||||||
tiles[idx] = value
|
|
||||||
this.props.onChange({
|
|
||||||
...this.props.source,
|
|
||||||
tiles: tiles
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTileUrls() {
|
|
||||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
|
||||||
const tiles = this.props.source.tiles || []
|
|
||||||
return tiles.map((tileUrl, tileIndex) => {
|
|
||||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={styleSpec.latest.source_vector.tiles.doc}>
|
|
||||||
<StringInput
|
|
||||||
value={tileUrl}
|
|
||||||
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div>
|
|
||||||
{this.renderTileUrls()}
|
|
||||||
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
|
|
||||||
<NumberInput
|
|
||||||
value={this.props.source.minzoom || 0}
|
|
||||||
onChange={minzoom => this.props.onChange({
|
|
||||||
...this.props.source,
|
|
||||||
minzoom: minzoom
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
|
|
||||||
<NumberInput
|
|
||||||
value={this.props.source.maxzoom || 22}
|
|
||||||
onChange={maxzoom => this.props.onChange({
|
|
||||||
...this.props.source,
|
|
||||||
maxzoom: maxzoom
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GeoJSONSourceEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
source: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
|
|
||||||
<StringInput
|
|
||||||
value={this.props.source.data}
|
|
||||||
onChange={data => this.props.onChange({
|
|
||||||
...this.props.source,
|
|
||||||
data: data
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SourceTypeEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mode: PropTypes.string.isRequired,
|
|
||||||
source: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const commonProps = {
|
|
||||||
source: this.props.source,
|
|
||||||
onChange: this.props.onChange,
|
|
||||||
}
|
|
||||||
switch(this.props.mode) {
|
|
||||||
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
|
||||||
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
|
||||||
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
|
||||||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
|
||||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SourceTypeEditor
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 8,
|
|
||||||
"name": "Empty Style",
|
|
||||||
"metadata": {
|
|
||||||
"mapbox:autocomposite": false,
|
|
||||||
"mapbox:type": "template",
|
|
||||||
"maputnik:renderer": "mbgljs",
|
|
||||||
"openmaptiles:version": "3.x"
|
|
||||||
},
|
|
||||||
"sources": { },
|
|
||||||
"glyphs": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"sprites": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"layers": []
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
{
|
|
||||||
"line": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"line-opacity",
|
|
||||||
"line-color",
|
|
||||||
"line-width",
|
|
||||||
"line-offset",
|
|
||||||
"line-blur",
|
|
||||||
"line-dasharray",
|
|
||||||
"line-pattern",
|
|
||||||
"line-translate",
|
|
||||||
"line-translate-anchor",
|
|
||||||
"line-gap-width"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Layout properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"line-cap",
|
|
||||||
"line-join",
|
|
||||||
"line-miter-limit",
|
|
||||||
"line-round-limit"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"background": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"background-color",
|
|
||||||
"background-pattern",
|
|
||||||
"background-opacity"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"fill": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"fill-opacity",
|
|
||||||
"fill-color",
|
|
||||||
"fill-antialias",
|
|
||||||
"fill-outline-color",
|
|
||||||
"fill-pattern",
|
|
||||||
"fill-translate",
|
|
||||||
"fill-translate-anchor"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"fill-extrusion": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"fill-extrusion-opacity",
|
|
||||||
"fill-extrusion-color",
|
|
||||||
"fill-extrusion-translate",
|
|
||||||
"fill-extrusion-translate-anchor",
|
|
||||||
"fill-extrusion-pattern",
|
|
||||||
"fill-extrusion-height",
|
|
||||||
"fill-extrusion-base"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"circle": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"circle-color",
|
|
||||||
"circle-opacity",
|
|
||||||
"circle-stroke-color",
|
|
||||||
"circle-stroke-opacity",
|
|
||||||
"circle-blur",
|
|
||||||
"circle-radius",
|
|
||||||
"circle-stroke-width",
|
|
||||||
"circle-pitch-scale",
|
|
||||||
"circle-translate",
|
|
||||||
"circle-translate-anchor"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"symbol": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "General layout properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"symbol-placement",
|
|
||||||
"symbol-spacing",
|
|
||||||
"symbol-avoid-edges"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Text layout properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"text-field",
|
|
||||||
"text-font",
|
|
||||||
"text-size",
|
|
||||||
"text-line-height",
|
|
||||||
"text-padding",
|
|
||||||
"text-allow-overlap",
|
|
||||||
"text-ignore-placement",
|
|
||||||
"text-pitch-alignment",
|
|
||||||
"text-rotation-alignment",
|
|
||||||
"text-max-width",
|
|
||||||
"text-letter-spacing",
|
|
||||||
"text-justify",
|
|
||||||
"text-anchor",
|
|
||||||
"text-max-angle",
|
|
||||||
"text-rotate",
|
|
||||||
"text-keep-upright",
|
|
||||||
"text-transform",
|
|
||||||
"text-offset",
|
|
||||||
"text-optional"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Icon layout properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"icon-allow-overlap",
|
|
||||||
"icon-ignore-placement",
|
|
||||||
"icon-optional",
|
|
||||||
"icon-rotation-alignment",
|
|
||||||
"icon-size",
|
|
||||||
"icon-text-fit",
|
|
||||||
"icon-text-fit-padding",
|
|
||||||
"icon-image",
|
|
||||||
"icon-rotate",
|
|
||||||
"icon-padding",
|
|
||||||
"icon-keep-upright",
|
|
||||||
"icon-offset"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Text paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"text-color",
|
|
||||||
"text-opacity",
|
|
||||||
"text-halo-color",
|
|
||||||
"text-halo-width",
|
|
||||||
"text-halo-blur",
|
|
||||||
"text-translate",
|
|
||||||
"text-translate-anchor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Icon paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"icon-color",
|
|
||||||
"icon-opacity",
|
|
||||||
"icon-halo-color",
|
|
||||||
"icon-halo-width",
|
|
||||||
"icon-halo-blur",
|
|
||||||
"icon-translate",
|
|
||||||
"icon-translate-anchor"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"raster": {
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Paint properties",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"raster-opacity",
|
|
||||||
"raster-hue-rotate",
|
|
||||||
"raster-brightness-min",
|
|
||||||
"raster-brightness-max",
|
|
||||||
"raster-saturation",
|
|
||||||
"raster-contrast",
|
|
||||||
"raster-fade-duration"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "klokantech-basic",
|
|
||||||
"title": "Klokantech Basic",
|
|
||||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "dark-matter",
|
|
||||||
"title": "Dark Matter",
|
|
||||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "positron",
|
|
||||||
"title": "Positron",
|
|
||||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "osm-bright",
|
|
||||||
"title": "OSM Bright",
|
|
||||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "osm-liberty",
|
|
||||||
"title": "OSM Liberty",
|
|
||||||
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
|
|
||||||
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empty-style",
|
|
||||||
"title": "Empty Style",
|
|
||||||
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
|
|
||||||
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "mapbox-satellite",
|
|
||||||
"title": "Mapbox Satellite",
|
|
||||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-satellite.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "mapbox-bright",
|
|
||||||
"title": "Mapbox Bright",
|
|
||||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-bright.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "mapbox-basic",
|
|
||||||
"title": "Mapbox Basic",
|
|
||||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-basic.png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"mapbox-streets": {
|
|
||||||
"type": "vector",
|
|
||||||
"url": "mapbox://mapbox.mapbox-streets-v7",
|
|
||||||
"title": "Mapbox Streets"
|
|
||||||
},
|
|
||||||
"openmaptiles": {
|
|
||||||
"type": "vector",
|
|
||||||
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
|
|
||||||
"title": "OpenMapTiles"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w",
|
|
||||||
"openmaptiles": "Og58UhhtiiTaLVlPtPgs"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |