diff --git a/angular.json b/angular.json index 935af78..5cd350e 100644 --- a/angular.json +++ b/angular.json @@ -120,7 +120,8 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "browserTarget": "demo:build" + "browserTarget": "demo:build", + "host": "0.0.0.0" }, "configurations": { "production": { @@ -149,4 +150,4 @@ } }}, "defaultProject": "library" -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index a797eba..026ebe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1219,9 +1219,9 @@ "dev": true }, "@babel/runtime": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz", - "integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", + "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", "dev": true, "requires": { "regenerator-runtime": "^0.13.2" @@ -1344,15 +1344,38 @@ "fastq": "^1.6.0" } }, + "@octokit/auth-token": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz", + "integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==", + "dev": true, + "requires": { + "@octokit/types": "^2.0.0" + } + }, + "@octokit/core": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-2.4.2.tgz", + "integrity": "sha512-fUx/Qt774cgiPhb3HRKfdl6iufVL/ltECkwkCg373I4lIPYvAPY4cbidVZqyVqHI+ThAIlFlTW8FT4QHChv3Sg==", + "dev": true, + "requires": { + "@octokit/auth-token": "^2.4.0", + "@octokit/graphql": "^4.3.1", + "@octokit/request": "^5.3.1", + "@octokit/types": "^2.0.0", + "before-after-hook": "^2.1.0", + "universal-user-agent": "^5.0.0" + } + }, "@octokit/endpoint": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz", - "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.3.tgz", + "integrity": "sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ==", "dev": true, "requires": { "@octokit/types": "^2.0.0", "is-plain-object": "^3.0.0", - "universal-user-agent": "^4.0.0" + "universal-user-agent": "^5.0.0" }, "dependencies": { "is-plain-object": { @@ -1372,10 +1395,57 @@ } } }, + "@octokit/graphql": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz", + "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==", + "dev": true, + "requires": { + "@octokit/request": "^5.3.0", + "@octokit/types": "^2.0.0", + "universal-user-agent": "^4.0.0" + }, + "dependencies": { + "universal-user-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "dev": true, + "requires": { + "os-name": "^3.1.0" + } + } + } + }, + "@octokit/plugin-paginate-rest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.0.1.tgz", + "integrity": "sha512-xtW3AQoGDD0un/AkPjIndTdFO+O/My0I15TArvrbJirBCV91R1ElrE3gRcsUJENP3t/vveiQ9C6XQjo9sS2xQg==", + "dev": true, + "requires": { + "@octokit/types": "^2.0.1" + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz", + "integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==", + "dev": true + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.2.0.tgz", + "integrity": "sha512-k+RLsegQn4s0wvAFYuk3R18FVKRg3ktvzIGW6MkmrSiSXBwYfaEsv4CuPysyef0DL+74DRj/X9MLJYlbleUO+Q==", + "dev": true, + "requires": { + "@octokit/types": "^2.0.1", + "deprecation": "^2.3.1" + } + }, "@octokit/request": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz", - "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.2.tgz", + "integrity": "sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g==", "dev": true, "requires": { "@octokit/endpoint": "^5.5.0", @@ -1385,7 +1455,7 @@ "is-plain-object": "^3.0.0", "node-fetch": "^2.3.0", "once": "^1.4.0", - "universal-user-agent": "^4.0.0" + "universal-user-agent": "^5.0.0" }, "dependencies": { "is-plain-object": { @@ -1406,9 +1476,9 @@ } }, "@octokit/request-error": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz", - "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", "dev": true, "requires": { "@octokit/types": "^2.0.0", @@ -1417,29 +1487,21 @@ } }, "@octokit/rest": { - "version": "16.36.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.36.0.tgz", - "integrity": "sha512-zoZj7Ya4vWBK4fjTwK2Cnmu7XBB1p9ygSvTk2TthN6DVJXM4hQZQoAiknWFLJWSTix4dnA3vuHtjPZbExYoCZA==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-17.0.0.tgz", + "integrity": "sha512-nSlmyy1DBEOsC4voRbk/SN56V/iuZfxZzjFFz+ocb2MAYwHC+z1TyVOMV9W630dVn9ukioJO34VD5NSYwcgFWg==", "dev": true, "requires": { - "@octokit/request": "^5.2.0", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^2.0.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^4.0.0" + "@octokit/core": "^2.4.0", + "@octokit/plugin-paginate-rest": "^2.0.0", + "@octokit/plugin-request-log": "^1.0.0", + "@octokit/plugin-rest-endpoint-methods": "^3.0.0" } }, "@octokit/types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.0.2.tgz", - "integrity": "sha512-StASIL2lgT3TRjxv17z9pAqbnI7HGu9DrJlg3sEBFfCLaMEqp+O3IQPUF6EZtQ4xkAu2ml6kMBBCtGxjvmtmuQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.2.0.tgz", + "integrity": "sha512-iEeW3XlkxeM/CObeoYvbUv24Oe+DldGofY+3QyeJ5XKKA6B+V94ePk14EDCarseWdMs6afKZPv3dFq8C+SY5lw==", "dev": true, "requires": { "@types/node": ">= 8" @@ -1526,9 +1588,9 @@ } }, "@semantic-release/commit-analyzer": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-6.3.3.tgz", - "integrity": "sha512-Pyv1ZL2u5AIOY4YbxFCAB5J1PEh5yON8ylbfiPiriDGGW6Uu1U3Y8lysMtWu+FUD5x7tSnyIzhqx0+fxPxqbgw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-8.0.1.tgz", + "integrity": "sha512-5bJma/oB7B4MtwUkZC2Bf7O1MHfi4gWe4mA+MIQ3lsEV0b422Bvl1z5HRpplDnMLHH3EXMoRdEng6Ds5wUqA3A==", "dev": true, "requires": { "conventional-changelog-angular": "^5.0.0", @@ -1536,7 +1598,8 @@ "conventional-commits-parser": "^3.0.7", "debug": "^4.0.0", "import-from": "^3.0.0", - "lodash": "^4.17.4" + "lodash": "^4.17.4", + "micromatch": "^4.0.2" }, "dependencies": { "debug": { @@ -1557,6 +1620,16 @@ "resolve-from": "^5.0.0" } }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1770,22 +1843,22 @@ } }, "@semantic-release/github": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-5.5.5.tgz", - "integrity": "sha512-Wo9OIULMRydbq+HpFh9yiLvra1XyEULPro9Tp4T5MQJ0WZyAQ3YQm74IdT8Pe/UmVDq2nfpT1oHrWkwOc4loHg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-7.0.4.tgz", + "integrity": "sha512-qQi41eGIa/tne7T8rvQK+xJNoyadOmd5mVsNZUUqZCVueiUkCItspJ7Mgy5ZWuhwlo28+hKeT/4zJ6MIG6er2Q==", "dev": true, "requires": { - "@octokit/rest": "^16.27.0", + "@octokit/rest": "^17.0.0", "@semantic-release/error": "^2.2.0", "aggregate-error": "^3.0.0", "bottleneck": "^2.18.1", "debug": "^4.0.0", "dir-glob": "^3.0.0", "fs-extra": "^8.0.0", - "globby": "^10.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "issue-parser": "^5.0.0", + "globby": "^11.0.0", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "issue-parser": "^6.0.0", "lodash": "^4.17.4", "mime": "^2.4.3", "p-filter": "^2.0.0", @@ -1793,6 +1866,15 @@ "url-join": "^4.0.0" }, "dependencies": { + "agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "dev": true, + "requires": { + "debug": "4" + } + }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1818,40 +1900,38 @@ } }, "globby": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", - "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz", + "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==", "dev": true, "requires": { - "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", "slash": "^3.0.0" } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "dev": true, "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "agent-base": "6", + "debug": "4" } }, "ignore": { @@ -2023,9 +2103,9 @@ } }, "@semantic-release/release-notes-generator": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-7.3.5.tgz", - "integrity": "sha512-LGjgPBGjjmjap/76O0Md3wc04Y7IlLnzZceLsAkcYRwGQdRPTTFUJKqDQTuieWTs7zfHzQoZqsqPfFxEN+g2+Q==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-9.0.1.tgz", + "integrity": "sha512-bOoTiH6SiiR0x2uywSNR7uZcRDl22IpZhj+Q5Bn0v+98MFtOMhCxFhbrKQjhbYoZw7vps1mvMRmFkp/g6R9cvQ==", "dev": true, "requires": { "conventional-changelog-angular": "^5.0.0", @@ -2147,6 +2227,12 @@ "defer-to-connect": "^1.0.1" } }, + "@tootallnate/once": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz", + "integrity": "sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -3009,12 +3095,6 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", - "dev": true - }, "autoprefixer": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.0.tgz", @@ -3593,12 +3673,6 @@ "https-proxy-agent": "^2.2.1" } }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", - "dev": true - }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -5340,12 +5414,12 @@ "dev": true }, "env-ci": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-4.5.2.tgz", - "integrity": "sha512-lS+edpNp2+QXEPkx6raEMIjKxKKWnJ4+VWzovYJ2NLYiJAYenSAXotFfVdgaFxdbVnvAbUI8epQDa1u12ERxfQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-5.0.1.tgz", + "integrity": "sha512-xXgohoOAFFF1Y3EdsSKP7olyH/DLS6ZD3aglV6mDFAXBqBXLJSsZLrOZdYfDs5mOmgNaP3YYynObzwF3QkC24g==", "dev": true, "requires": { - "execa": "^3.2.0", + "execa": "^4.0.0", "java-properties": "^1.0.0" }, "dependencies": { @@ -5361,9 +5435,9 @@ } }, "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.0.tgz", + "integrity": "sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -5373,7 +5447,6 @@ "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", - "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } @@ -5411,12 +5484,6 @@ "mimic-fn": "^2.1.0" } }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7472,6 +7539,12 @@ "glogg": "^1.0.0" } }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=", + "dev": true + }, "handle-thing": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", @@ -8483,9 +8556,9 @@ "dev": true }, "issue-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-5.0.0.tgz", - "integrity": "sha512-q/16W7EPHRL0FKVz9NU++TUsoygXGj6JOi88oulyAcQG+IEZ0T6teVdE+VLbe19OfL/tbV8Wi3Dfo0HedeHW0Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", + "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", "dev": true, "requires": { "lodash.capitalize": "^4.2.1", @@ -9980,12 +10053,6 @@ "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=", "dev": true }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -10033,12 +10100,6 @@ "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "dev": true }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, "lodash.tail": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", @@ -10078,12 +10139,6 @@ "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", "dev": true }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, "lodash.uniqby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", @@ -10264,23 +10319,90 @@ } }, "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.0.tgz", + "integrity": "sha512-MyUe+T/Pw4TZufHkzAfDj6HarCBWia2y27/bhuYkTaiUnfDYFnCP3KUN+9oM7Wi6JA2rymtVYbQu3spE0GCmxQ==", "dev": true }, "marked-terminal": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.3.0.tgz", - "integrity": "sha512-+IUQJ5VlZoAFsM5MHNT7g3RHSkA3eETqhRCdXv4niUMAKHQ7lb1yvAcuGPmm4soxhmtX13u4Li6ZToXtvSEH+A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-4.0.0.tgz", + "integrity": "sha512-mzU3VD7aVz12FfGoKFAceijehA6Ocjfg3rVimvJbFAB/NOYCsuzRVtq3PSFdPmWI5mhdGeEh3/aMJ5DSxAz94Q==", "dev": true, "requires": { - "ansi-escapes": "^3.1.0", + "ansi-escapes": "^4.3.0", "cardinal": "^2.1.1", - "chalk": "^2.4.1", + "chalk": "^3.0.0", "cli-table": "^0.3.1", - "node-emoji": "^1.4.1", - "supports-hyperlinks": "^1.0.1" + "node-emoji": "^1.10.0", + "supports-hyperlinks": "^2.0.0" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } } }, "matchdep": { @@ -14866,12 +14988,6 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -15028,6 +15144,12 @@ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", "dev": true }, + "p-each-series": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", + "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", + "dev": true + }, "p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -16794,21 +16916,21 @@ } }, "semantic-release": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-15.14.0.tgz", - "integrity": "sha512-Cn43W35AOLY0RMcDbtwhJODJmWg6YCs1+R5jRQsTmmkEGzkV4B2F/QXkjVZpl4UbH91r93GGH0xhoq9kh7I5PA==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-17.0.4.tgz", + "integrity": "sha512-5y9QRSrZtdvACmlpX5DvEVsvFuKRDUVn7JVJFxPVLGrGofDf1d0M/+hA1wFmCjiJZ+VCY8bYaSqVqF14KCF9rw==", "dev": true, "requires": { - "@semantic-release/commit-analyzer": "^6.1.0", + "@semantic-release/commit-analyzer": "^8.0.0", "@semantic-release/error": "^2.2.0", - "@semantic-release/github": "^5.1.0", - "@semantic-release/npm": "^5.0.5", - "@semantic-release/release-notes-generator": "^7.1.2", + "@semantic-release/github": "^7.0.0", + "@semantic-release/npm": "^7.0.0", + "@semantic-release/release-notes-generator": "^9.0.0", "aggregate-error": "^3.0.0", "cosmiconfig": "^6.0.0", "debug": "^4.0.0", - "env-ci": "^4.0.0", - "execa": "^3.2.0", + "env-ci": "^5.0.0", + "execa": "^4.0.0", "figures": "^3.0.0", "find-versions": "^3.0.0", "get-stream": "^5.0.0", @@ -16816,17 +16938,40 @@ "hook-std": "^2.0.0", "hosted-git-info": "^3.0.0", "lodash": "^4.17.15", - "marked": "^0.7.0", - "marked-terminal": "^3.2.0", - "p-locate": "^4.0.0", + "marked": "^0.8.0", + "marked-terminal": "^4.0.0", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", "p-reduce": "^2.0.0", "read-pkg-up": "^7.0.0", "resolve-from": "^5.0.0", - "semver": "^6.0.0", + "semver": "^7.1.1", + "semver-diff": "^3.1.1", "signale": "^1.2.1", "yargs": "^15.0.1" }, "dependencies": { + "@semantic-release/npm": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-7.0.3.tgz", + "integrity": "sha512-3wOXMtAdJkaAnW5183iSmWSimtUmOx7m6g/DWPYRs2Gq6iyB+ztMmhgwbn6luNcM9t6pGbgHvRPEXpWkygMxCA==", + "dev": true, + "requires": { + "@semantic-release/error": "^2.2.0", + "aggregate-error": "^3.0.0", + "execa": "^4.0.0", + "fs-extra": "^8.0.0", + "lodash": "^4.17.15", + "nerf-dart": "^1.0.0", + "normalize-url": "^5.0.0", + "npm": "^6.10.3", + "rc": "^1.2.8", + "read-pkg": "^5.0.0", + "registry-auth-token": "^4.0.0", + "semver": "^7.1.2", + "tempy": "^0.4.0" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -16834,9 +16979,9 @@ "dev": true }, "ansi-styles": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", - "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { "@types/color-name": "^1.1.1", @@ -16893,6 +17038,12 @@ "which": "^2.0.1" } }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -16909,9 +17060,9 @@ "dev": true }, "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.0.tgz", + "integrity": "sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -16921,15 +17072,14 @@ "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", - "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "figures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", - "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -17008,12 +17158,28 @@ "p-locate": "^4.1.0" } }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "normalize-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-5.0.0.tgz", + "integrity": "sha512-bAEm2fx8Dq/a35Z6PIRkkBBJvR56BbEJvhpNtvCZ4W9FyORSna77fn+xtYFjqk5JpBS+fMnAOG/wFgkQBmB7hw==", + "dev": true + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -17032,12 +17198,6 @@ "mimic-fn": "^2.1.0" } }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -17086,6 +17246,14 @@ "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } } }, "require-main-filename": { @@ -17100,6 +17268,29 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==", + "dev": true + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -17135,12 +17326,38 @@ "ansi-regex": "^5.0.0" } }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true + }, + "tempy": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.4.0.tgz", + "integrity": "sha512-mKnScm8aXv+cG6l1Nzp6mERGgC4UblbPnSDeQp83JgZ7xqDcnl+7u3+6zXnf1UE7YluDUTEIna1iKYwCSaOk9g==", + "dev": true, + "requires": { + "temp-dir": "^2.0.0", + "type-fest": "^0.10.0", + "unique-string": "^2.0.0" + } + }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz", + "integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==", "dev": true }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -17162,9 +17379,9 @@ } }, "yargs": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.0.2.tgz", - "integrity": "sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", + "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -18353,20 +18570,29 @@ } }, "supports-hyperlinks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", - "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", "dev": true, "requires": { - "has-flag": "^2.0.0", - "supports-color": "^5.0.0" + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" }, "dependencies": { "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -19000,9 +19226,9 @@ } }, "universal-user-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", - "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz", + "integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==", "dev": true, "requires": { "os-name": "^3.1.0" diff --git a/package.json b/package.json index a9acb3b..fac0144 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "gulp": "^4.0.2", "gulp-string-replace": "^1.1.2", "gulp-util": "^3.0.8", + "hammerjs": "^2.0.8", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~4.1.0", @@ -54,7 +55,7 @@ "karma-jasmine-html-reporter": "^1.4.0", "ng-packagr": "^5.1.0", "protractor": "~5.4.0", - "semantic-release": "^15.13.12", + "semantic-release": "^17.0.2", "ts-node": "^7.0.1", "tsickle": "^0.35.0", "tslint": "~5.15.0", diff --git a/projects/demo/src/app/app.component.html b/projects/demo/src/app/app.component.html index f172862..166d089 100644 --- a/projects/demo/src/app/app.component.html +++ b/projects/demo/src/app/app.component.html @@ -1,17 +1,23 @@
-
-
- Loading Map... +
+ Loading Map... +
+
{{ model.zoom * 100 }}%
+
{{ model.center | json }}
+
+

- {{ model.zoom * 100 }}%

- {{ model.center | json }}
-
diff --git a/projects/demo/src/app/app.component.scss b/projects/demo/src/app/app.component.scss index 937f705..7d0c39e 100644 --- a/projects/demo/src/app/app.component.scss +++ b/projects/demo/src/app/app.component.scss @@ -83,3 +83,20 @@ button { margin: .25rem; flex: 1 } + +.map svg { + * { + shape-rendering: optimizeSpeed; + } +} + +.map-outlet { + &.zooming { + svg { + [id*="plant"], + [id*="chair"] { + display: none; + } + } + } +} diff --git a/projects/demo/src/app/app.component.ts b/projects/demo/src/app/app.component.ts index ce1d614..dd39a5a 100644 --- a/projects/demo/src/app/app.component.ts +++ b/projects/demo/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component, ViewEncapsulation } from '@angular/core'; -import { MapRangeComponent } from 'projects/library/src/lib/components/overlays/map-range/map-range.component'; import { MapPinComponent } from 'projects/library/src/lib/components/overlays/map-pin/map-pin.component'; +import { MapRadiusComponent } from 'projects/library/src/lib/components/overlays/map-radius/map-radius.component'; import * as dayjs from 'dayjs'; @@ -18,7 +18,15 @@ export class AppComponent { this.updatePointsOfInterest(); this.model.show = {}; this.model.map = {}; - this.model.map.src = 'assets/australia.svg'; + this.model.map.src = 'assets/level_10.svg'; + this.model.map.text = [ + { id: 'area-10.06-status', content: 'Meeting Room\n10.06' }, + { id: 'area-10.05-status', content: 'Meeting Room\n10.05' }, + { id: 'scanner-2', content: 'Scanner', show_after_zoom: 2, styles: { 'color': 'red' } } + ]; + this.model.map.listeners = [ + { id: 'area-10.06-status', event: 'click', callback: () => console.log('Clicked: 10.06') } + ]; this.model.zoom = 1; this.model.center = { x: 0.25, y: 0.75 }; this.model.count = Array(3).fill(0); @@ -36,7 +44,7 @@ export class AppComponent { public toggleMap() { this.model.map.src = - this.model.map.src.indexOf('180') >= 0 ? 'assets/level_01.svg' : 'assets/australia-180-rot.svg'; + this.model.map.src.indexOf('180') >= 0 ? 'assets/level_10.svg' : 'assets/australia-180-rot.svg'; } public zoom(value: number) { @@ -49,7 +57,7 @@ export class AppComponent { this.model.zoom = +(this.model.zoom * (1 / (1 - value / 100))).toFixed(5); if (this.model.zoom < 1) { this.model.zoom = 1; - } + } } } @@ -57,45 +65,50 @@ export class AppComponent { this.model.map.poi = []; if (this.model.show.radius) { this.model.map.poi.push({ - id: 'Nyada', coordinates: { x: 3000, y: 3000 }, - content: MapRangeComponent, - data: { text: `I'm somewhere in this circle`, diameter: 10 } + content: MapRadiusComponent, + data: { text: `I'm somewhere in this circle`, diameter: 5 } }); } if (this.model.show.pin) { this.model.fixed = !this.model.fixed; const fixed = this.model.fixed; this.model.map.poi.push({ - id: fixed ? 'AU-NSW' : 'Nyada', - coordinates: fixed ? null : { x: 5000, y: 7500 }, + id: fixed ? 'area-10.05-status' : undefined, + coordinates: fixed ? null : { x: 7500, y: 1000 }, content: MapPinComponent, data: { text: fixed ? 'NSW is here' : `I'm currently round here` } }); - const focus: any = {}; + let focus: any = null; if (fixed) { - focus.id = 'AU-NSW'; + focus = 'area-10.05-status'; } else { - focus.coordinates = { x: 5000, y: 7500 }; + focus = { x: 0.75, y: 0.25 }; } this.model.map.focus = focus; this.model.map.styles = { - '#AU-NSW': { fill: ['#123456', '#345612', '#561234'][Math.floor(Math.random() * 3)] }, - '#AU.NT:hover': { - fill: ['#654321', '#436521', '#216543'][Math.floor(Math.random() * 3)], + '#area-10.05-status': { + fill: ['#123456', '#345612', '#561234'][Math.floor(Math.random() * 3)], transition: 'fill 200ms' + }, + '#area-10.05-status:hover': { + fill: ['#654321', '#436521', '#216543'][Math.floor(Math.random() * 3)] } }; } if (this.model.show.hover) { this.model.map.poi.push({ - id: 'AU.NT', + id: 'area-10.05-status', coordinates: null, content: MapPinComponent, data: { text: 'This state is WA' } }); } } + + log(content) { + console.log('Map Event:', content); + } } diff --git a/projects/demo/src/app/app.module.ts b/projects/demo/src/app/app.module.ts index e1a3531..0494a33 100644 --- a/projects/demo/src/app/app.module.ts +++ b/projects/demo/src/app/app.module.ts @@ -5,6 +5,8 @@ import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { AInteractiveMapModule } from 'projects/library/src/public-api'; +import 'hammerjs'; + @NgModule({ declarations: [ AppComponent diff --git a/projects/demo/src/assets/level_10.svg b/projects/demo/src/assets/level_10.svg new file mode 100644 index 0000000..d45afbd --- /dev/null +++ b/projects/demo/src/assets/level_10.svg @@ -0,0 +1 @@ +Lendlease_lvl_10_V6C5L04C5L04C5L04C5L04C5L04C5L04C5L04C5L04Goods LiftMaleToiletsFemaleToiletsGoods Lift LobbyWSocial Node(West)SocialNode(East)estStairsNose Meeting(West)Team StudioTeam StudioTeam StudioTeam StudioComms RoomTeam StudioTeamNose Meeting(East)StudioTeamStudioUATUATEastStairsElec RoomStoreHighPrint/ScanBinsStreetFHRFHRFHRFHRLift LobbyDefence RoomProject RoomMeeting RoomNOSE MEETING10.27NOSEMEETING10.10CommonRoomsCommonRooms diff --git a/projects/library/src/lib/classes/map-render-feature.spec.ts b/projects/library/src/lib/classes/map-render-feature.spec.ts new file mode 100644 index 0000000..aa9e6fa --- /dev/null +++ b/projects/library/src/lib/classes/map-render-feature.spec.ts @@ -0,0 +1,7 @@ +// import { MapRenderFeature } from './map-render-feature'; + +// describe('MapFeature', () => { +// it('should create an instance', () => { +// expect(new MapRenderFeature(null, null)).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/classes/map-render-feature.ts b/projects/library/src/lib/classes/map-render-feature.ts new file mode 100644 index 0000000..4f387d4 --- /dev/null +++ b/projects/library/src/lib/classes/map-render-feature.ts @@ -0,0 +1,57 @@ +import { TemplateRef, Type } from '@angular/core'; + +import { Point, RenderFeature } from '../helpers/type.helpers'; +import { RenderableMap } from './renderable-map'; +import { log } from '../settings'; + +export class MapRenderFeature { + /** ID of an element to render */ + public readonly id: string; + /** Coordinates with the map */ + public readonly coordinates: Point; + /** Content to render on the map */ + public readonly content: RenderFeature; + /** Data to pass to the content */ + public readonly data: any; + /** Data to pass to the content */ + public readonly show_after_zoom: number; + + /** Type of content being rendered by this feature */ + public get content_type(): 'component' | 'template' | 'html' { + return this.content instanceof Type + ? 'component' + : this.content instanceof TemplateRef + ? 'template' + : 'html'; + } + + constructor(data: { [name: string]: any }, map: RenderableMap) { + const coordinates = this.processCoordinates(data.id || data.coordinates, map); + this.id = data.id || JSON.stringify(coordinates); + this.coordinates = coordinates; + this.content = data.content; + this.data = data.data || data.styles; + this.show_after_zoom = data.show_after_zoom; + } + + private processCoordinates(data: string | Point, map: RenderableMap): Point { + if (!map) { return; } + if (typeof data === 'string') { + const element = map.element_map[data]; + if (element) { + return element.coordinates; + } else { + log('MAP', `No element for id "${data}"`, undefined, 'warn'); + } + } else { + if (data.x <= 1 && data.x >= 0 && data.y <= 1 && data.y >= 0) { + return data; + } else { + return { + x: data.x / 10000, + y: data.y / (10000 * map.dimensions.y) + }; + } + } + } +} diff --git a/projects/library/src/lib/classes/map-styles.spec.ts b/projects/library/src/lib/classes/map-styles.spec.ts new file mode 100644 index 0000000..8a9e81c --- /dev/null +++ b/projects/library/src/lib/classes/map-styles.spec.ts @@ -0,0 +1,7 @@ +// import { MapStyles } from './map-styles'; + +// describe('MapStyles', () => { +// it('should create an instance', () => { +// expect(new MapStyles({}, null)).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/classes/map-styles.ts b/projects/library/src/lib/classes/map-styles.ts new file mode 100644 index 0000000..701c2a2 --- /dev/null +++ b/projects/library/src/lib/classes/map-styles.ts @@ -0,0 +1,63 @@ +import { HashMap } from '../helpers/type.helpers'; +import { cleanCssSelector } from '../helpers/map.helpers'; +import { RenderableMap } from './renderable-map'; + +export class MapStyles { + /** Mapping of CSS selectors to override CSS properties */ + public readonly styles: HashMap>; + /** CSS string that can be injected into the DOM */ + private _css: string; + /** Element rendering the map styles */ + private _element: HTMLStyleElement; + /** CSS string that can be injected into the DOM */ + public get css(): string { + return this._css; + } + + constructor(styles: HashMap>, private map: RenderableMap) { + this.styles = styles; + this._css = this._processStyles(styles); + this._renderStyleElement(this.css); + } + + /** Cleanup map styles */ + public destroy() { + if (this._element) { + this._element.parentElement.removeChild(this._element); + delete this._element; + this._element = null; + } + } + + /** + * Convert style map into CSS string + * @param styles Mapping of CSS selectors to override CSS properties + */ + private _processStyles(styles: HashMap>): string { + let css = ''; + for (const selector in this.styles) { + if (this.styles.hasOwnProperty(selector)) { + let style = `.map[id="${this.map ? this.map.id : 'map-0'}"] ${cleanCssSelector(selector)} { `; + for (const property in this.styles[selector]) { + if (this.styles[selector][property]) { + style += `${property}: ${this.styles[selector][property]}; `; + } + } + style += '} '; + css += style; + } + } + return css; + } + + /** Render Style Element on the DOM */ + private _renderStyleElement(css: string) { + if (this.map) { + const element = document.createElement('style'); + element.id = `placeos-${this.map.id}`; + element.innerHTML = css; + document.head.appendChild(element); + this._element = element; + } + } +} diff --git a/projects/library/src/lib/classes/renderable-map.spec.ts b/projects/library/src/lib/classes/renderable-map.spec.ts new file mode 100644 index 0000000..e608631 --- /dev/null +++ b/projects/library/src/lib/classes/renderable-map.spec.ts @@ -0,0 +1,7 @@ +// import { RenderableMap } from './renderable-map'; + +// describe('RenderableMap', () => { +// it('should create an instance', () => { +// expect(new RenderableMap('', '')).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/classes/renderable-map.ts b/projects/library/src/lib/classes/renderable-map.ts new file mode 100644 index 0000000..f27a60c --- /dev/null +++ b/projects/library/src/lib/classes/renderable-map.ts @@ -0,0 +1,80 @@ +import { HashMap, Point } from '../helpers/type.helpers'; +import { getPosition } from '../helpers/map.helpers'; + +export interface MapElement { + readonly id: string; + readonly coordinates: Point; +} + +let COUNTER = 0; + +export class RenderableMap { + /** ID of the map */ + public readonly id: string; + /** URL of the map */ + public readonly url: string; + /** File contents of URL */ + public readonly raw_data: string; + /** List of available ID selectors in the map data */ + public readonly available_ids: readonly string[]; + /** Dimensions ratio of the map */ + public readonly dimensions: Point; + + /** Mapping of element id's to their locations with the map data */ + private _element_map: HashMap = {}; + + public get element_map(): HashMap { + return { ...this._element_map }; + } + + constructor(url: string, map_data: string) { + this.id = `map-${++COUNTER}`; + this.url = url; + const raw_data = this._cleanMapData(map_data); + this.raw_data = raw_data; + const { id_list, dimensions } = this._processMapData(); + this.available_ids = id_list; + this.dimensions = dimensions; + } + + /** Process map data and generate lookup table */ + private _processMapData() { + const element = document.createElement('div'); + element.style.setProperty('position', 'absolute'); + element.style.setProperty('top', '-9999px'); + element.style.setProperty('left', '-9999px'); + element.style.setProperty('height', '1000px'); + element.style.setProperty('width', '1000px'); + element.innerHTML = this.raw_data; + document.body.appendChild(element); + const svg_el: SVGElement = element.querySelector('svg'); + const box = svg_el.getBoundingClientRect(); + const dimensions = { x: 1, y: box.height / box.width }; + const id_elements = element.querySelectorAll('[id]'); + const id_list: string[] = []; + this._element_map = {}; + id_elements.forEach(el => { + const el_box = el.getBoundingClientRect(); + this._element_map[el.id] = { + id: el.id, + coordinates: getPosition(box, el_box) + }; + id_list.push(el.id); + }); + return { id_list, dimensions }; + } + + /** Clean map styles */ + private _cleanMapData(map_data: string) { + let raw_data = ''; + // Prevent non SVG files from being used + if (map_data.match(/<\/svg>/g)) { + // Prevent Adobe generic style names from being used + raw_data = map_data.replace(/cls\-/gm, `${this.id}-`); + raw_data = raw_data.replace(/\.map/gm, `svg .map`); + // Remove title tags and content from the map + raw_data = raw_data.replace(/.*<\/title>/gm, ''); + } + return raw_data; + } +} diff --git a/projects/library/src/lib/components/map-feature/map-feature.class.spec.ts b/projects/library/src/lib/components/map-feature/map-feature.class.spec.ts deleted file mode 100644 index e69de29..0000000 diff --git a/projects/library/src/lib/components/map-feature/map-feature.class.ts b/projects/library/src/lib/components/map-feature/map-feature.class.ts deleted file mode 100644 index c210fac..0000000 --- a/projects/library/src/lib/components/map-feature/map-feature.class.ts +++ /dev/null @@ -1,89 +0,0 @@ - -import { Injector, TemplateRef, Injectable } from '@angular/core'; -import { Subscription } from 'rxjs'; - -import { AMapComponent } from '../map/map.component'; -import { IReadonlyMapPoint, MapOverlayContent, IMapFeature } from '../map.interfaces'; - -export class AMapFeature<T = any> { - /** Map Element selector */ - readonly id: string; - /** Map coordinates */ - readonly coordinates: IReadonlyMapPoint; - /** Content to render at position */ - readonly content: MapOverlayContent; - /** Content render method. Determined automatically from `content` value */ - readonly method: 'component' | 'text' | 'template'; - /** Data to inject into the content template/component */ - private _data: T; - /** Inject for passing data into components */ - private _inject: Injector; - /** Location of the feature on the map */ - private _position: IReadonlyMapPoint; - - constructor(private _map: AMapComponent, private _injector: Injector, data: IMapFeature<T>) { - if (!this._map || !this._injector) { - throw new Error('Cannot build map feature without map or injector'); - } - this.id = data.id; - this.coordinates = data.coordinates; - this._data = data.data; - this.content = data.content; - - this.method = 'component'; - - if (typeof this.content === 'string') { - this.method = 'text'; - } else if (this.content instanceof TemplateRef) { - this.method = 'template'; - } - } - - /** Location of the feature on the map */ - public get position(): IReadonlyMapPoint { - return this._position || { x: 0, y: 0 }; - } - - public set position(position: IReadonlyMapPoint) { - this._position = position; - } - - /** Current zoom level of the map */ - public get zoom(): number { - return this._map.zoom || 0; - } - - /** Listen for changes to the map's zoom value */ - public zoomChanges(next: (v) => void): Subscription { - return this._map.zoomChange.subscribe(next); - } - - /** Center position of the map */ - public get center(): IReadonlyMapPoint { - return this._map.center || { x: .5, y: .5 }; - } - - /** Listen for changes to the map's center poistion */ - public centerChanges(next: (v) => void): Subscription { - return this._map.centerChange.subscribe(next); - } - - /** Data to inject into the content template/component */ - public get data(): any { - return this._data; - } - - public set data(data: any) { - this._data = data; - } - - /** Angular Injector for this map feature */ - public get injector(): Injector { - if (!this._inject) { - this._inject = Injector.create([ - { provide: AMapFeature, useValue: this } - ], this._injector); - } - return this._inject; - } -} diff --git a/projects/library/src/lib/components/map-outlet/map-outlet.component.html b/projects/library/src/lib/components/map-outlet/map-outlet.component.html new file mode 100644 index 0000000..9b692a1 --- /dev/null +++ b/projects/library/src/lib/components/map-outlet/map-outlet.component.html @@ -0,0 +1,26 @@ +<div + class="map-container" + #container + (window:resize)="updateContainerBox()" + map-input + [(zoom)]="zoom" + (zoomChange)="updateZoom($event)" + [(center)]="center" + (centerChange)="updateCenter($event)" + [map]="map" + [element]="map_element" + (click)="emitPointerPostion($event)" +> + <div + class="map-outlet" + #element + [class.zooming]="zoom !== local_zoom" + [style.width]="width" + [style.height]="height" + [style.transform]="'translate(' + transformX + '%, ' + transformY + '%)'" + > + <div class="map" *ngIf="map" [id]="map.id" [innerHTML]="map.raw_data | safe"></div> + <map-text-outlet [zoom]="local_zoom" [items]="text"></map-text-outlet> + <map-overlay-outlet [zoom]="local_zoom" [center]="local_center" [items]="features"></map-overlay-outlet> + </div> +</div> diff --git a/projects/library/src/lib/components/map-outlet/map-outlet.component.scss b/projects/library/src/lib/components/map-outlet/map-outlet.component.scss new file mode 100644 index 0000000..a807a04 --- /dev/null +++ b/projects/library/src/lib/components/map-outlet/map-outlet.component.scss @@ -0,0 +1,38 @@ + +:host, +.map-container { + position: relative; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.map-container { + width: calc(100% - 2em); + height: calc(100% - 2em); + margin: 1em; +} + +.map-outlet { + position: relative; + will-change: height, width, transform; + top: 50%; + left: 50%; + transform-origin: left top; + + * { + user-select: none; + } +} + +.map { + z-index: 1; +} + +map-text-outlet { + z-index: 2; +} + +map-overlay-outlet { + z-index: 3; +} diff --git a/projects/library/src/lib/components/map-outlet/map-outlet.component.spec.ts b/projects/library/src/lib/components/map-outlet/map-outlet.component.spec.ts new file mode 100644 index 0000000..bb99b78 --- /dev/null +++ b/projects/library/src/lib/components/map-outlet/map-outlet.component.spec.ts @@ -0,0 +1,24 @@ +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +// import { MapOutletComponent } from './map-outlet.component'; + +// describe('MapOutletComponent', () => { +// let component: MapOutletComponent; +// let fixture: ComponentFixture<MapOutletComponent>; + +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [MapOutletComponent] +// }).compileComponents(); +// })); + +// beforeEach(() => { +// fixture = TestBed.createComponent(MapOutletComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// }); + +// it('should create', () => { +// expect(component).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/components/map-outlet/map-outlet.component.ts b/projects/library/src/lib/components/map-outlet/map-outlet.component.ts new file mode 100644 index 0000000..cc08023 --- /dev/null +++ b/projects/library/src/lib/components/map-outlet/map-outlet.component.ts @@ -0,0 +1,260 @@ +import { + Component, + OnInit, + Input, + ViewChild, + ElementRef, + EventEmitter, + Output, + SimpleChanges, + OnChanges, + Renderer2, + OnDestroy +} from '@angular/core'; + +import { RenderableMap } from '../../classes/renderable-map'; +import { Point, MapEvent } from '../../helpers/type.helpers'; +import { eventToPoint, staggerChange, cleanCssSelector } from '../../helpers/map.helpers'; +import { MapRenderFeature } from '../../classes/map-render-feature'; +import { MapListener } from '../../helpers/map.interfaces'; +import { log } from '../../settings'; + +@Component({ + selector: 'a-map-outlet', + templateUrl: './map-outlet.component.html', + styleUrls: ['./map-outlet.component.scss'] +}) +export class MapOutletComponent implements OnInit, OnChanges, OnDestroy { + /** Details of the map */ + @Input() map: RenderableMap; + /** Zoom level of the map as a whole number. 1 = 100% zoom */ + @Input() public zoom: number; + /** + * Position of the center point of the component on the map displayed + * + * For example: + * + * { x: 0, y: 0 } + * Places the map top left corner in the middle of the component + * + * { x: 0.5, y: 0.5 } + * Places the center of the map in the middle of the component + * + * { x: 1, y: 1 } + * Places the bottom right corner of the map in the middle of the component + */ + @Input() public center: Point; + /** List of features to render over the map */ + @Input() public features: MapRenderFeature[] = []; + /** List of features to render over the map */ + @Input() public listeners: MapListener[] = []; + /** List of text to render over the map */ + @Input() public text: MapRenderFeature[] = []; + /** Emitter for changes to the zoom value */ + @Output() public zoomChange = new EventEmitter<number>(); + /** Emitter for changes to the center value */ + @Output() public centerChange = new EventEmitter<Point>(); + /** Emitter for changes to the zoom value */ + @Output() public events = new EventEmitter<MapEvent>(); + /** Local zoom value used to rendered the map */ + public local_zoom: number = 1; + /** Local zoom value used to rendered the map */ + public local_center: Point = { x: 0.5, y: 0.5 }; + + /** Element reference to the map display element */ + @ViewChild('element', { static: true }) public map_element: ElementRef<HTMLDivElement>; + /** Element reference to the map container element */ + @ViewChild('container', { static: true }) private _container: ElementRef<HTMLDivElement>; + /** Bounding box for the map */ + private _box: ClientRect; + /** Promise for handling changes to zoom values */ + private zoom_promise: Promise<void>; + /** Promise for handling changes to center position values */ + private center_promise: Promise<void>; + /** Store of latest difference change between zoom values */ + private _zoom_diff: number; + + private dimensions: Point = { x: 1, y: 1 }; + /** List of active listeners */ + private _event_handlers: (() => void)[] = []; + + /** Width of the map outlet container */ + public get width(): string { + if (!this.map) { + return '0'; + } + return `${(this.local_zoom * this.size_dimension * .9).toFixed(2)}px`; + } + /** Height of the map outlet container */ + public get height(): string { + if (!this.map) { + return '0'; + } + const height = (this.local_zoom * this.size_dimension * this.map.dimensions.y) * .9; + return `${height.toFixed(2)}px`; + } + /** Position of the map outlet container */ + public get transformX(): number { + return -(this.local_center ? this.local_center.x : 0.5) * 100; + } + /** Position of the map outlet container */ + public get transformY(): number { + return -(this.local_center ? this.local_center.y : 0.5) * 100; + } + + /** Get width of the map render box */ + public get size_dimension(): number { + return this._box + ? this.dimensions.y < this.map.dimensions.y + ? this._box.width * (this.dimensions.y / this.map.dimensions.y) + : this._box.width + : 100; + } + + constructor(private _renderer: Renderer2) { } + + public ngOnInit(): void { + this.updateContainerBox(); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes.zoom) { + this.staggerZoom(); + } + if (changes.center) { + this.staggerCenter(); + } + if (changes.listeners || changes.map) { + this.updateListeners(); + } + } + + public ngOnDestroy(): void { + this._event_handlers.forEach(item => item ? item() : ''); + delete this._event_handlers; + this._event_handlers = []; + } + + /** + * Emitted the position of the mouse click relative to the map + * @param event Mouse or touch event + */ + public emitPointerPostion(event: MouseEvent | TouchEvent) { + const point = eventToPoint(event); + const box = this.map_element.nativeElement.getBoundingClientRect(); + const position = { + x: +((point.x - box.left) / box.width).toFixed(4), + y: +((point.y - box.top) / box.height).toFixed(4) + }; + this.events.emit({ type: 'click', metadata: position } as MapEvent); + } + + /** Update the bound box of the container bounding box */ + public updateContainerBox() { + if (this._container && this._container.nativeElement) { + this._box = this._container.nativeElement.getBoundingClientRect(); + this.dimensions = { x: 1, y: this._box.height / this._box.width }; + } + } + + + /** + * Update the zoom level of the map + * @param new_zoom New zoom level + */ + public updateZoom(new_zoom: number) { + this.zoomChange.emit(new_zoom); + this.staggerZoom(); + } + + + /** + * Stagger the changes of the zoom value to have it animate smoothly + */ + private staggerZoom() { + this._zoom_diff = Math.abs(this.zoom - this.local_zoom); + if (!this.zoom_promise) { + this.zoom_promise = staggerChange(this.zoom - this.local_zoom, () => { + let change = this.zoom - this.local_zoom; + const direction = change < 0 ? -1 : 1; + const change_value = Math.max(0.02, Math.min(0.75, Math.abs(this._zoom_diff) / 10)); + this.local_zoom += + this._zoom_diff > change_value ? (direction < 0 ? -change_value : change_value) : change; + this.local_zoom = Math.max(1, Math.min(10, this.local_zoom)); + const not_done = Math.abs(change) < change_value ? 0 : change; + if (!not_done) { + this.local_zoom = this.zoom; + } + return not_done; + }); + this.zoom_promise.then(() => (this.zoom_promise = null)); + } + } + + /** + * Update the center location of the map + * @param new_center New center coordinates + */ + public updateCenter(new_center: Point) { + this.centerChange.emit(new_center); + this.staggerCenter(); + } + + /** + * Stagger the changes of the center values to have it animate smoothly + */ + private staggerCenter() { + if (!this.center_promise) { + this.center_promise = staggerChange(1, () => { + const change = { + x: this.center.x - this.local_center.x, + y: this.center.y - this.local_center.y + }; + const direction = { + x: change.x < 0 ? -1 : 1, + y: change.y < 0 ? -1 : 1 + }; + const change_value = { + x: Math.max(0.01, Math.min(0.05, Math.abs(change.x) / 5)), + y: Math.max(0.01, Math.min(0.05, Math.abs(change.y) / 5)) + }; + this.local_center = { + x: + this.local_center.x + + (Math.abs(change.x) > change_value.x ? (direction.x < 0 ? -1 : 1) * change_value.x : change.x), + y: + this.local_center.y + + (Math.abs(change.y) > change_value.y ? (direction.y < 0 ? -1 : 1) * change_value.y : change.y) + }; + return Math.abs(change.x) < change_value.x && Math.abs(change.y) < change_value.y ? 0 : 1; + }); + this.center_promise.then(() => (this.center_promise = null)); + } + } + + /** + * Update event handlers for map elements + */ + private updateListeners(): void { + if (!this.map_element || !this.map) { + setTimeout(() => this.updateListeners(), 50); + return; + } + this._event_handlers.forEach(item => item ? item() : ''); + delete this._event_handlers; + this._event_handlers = []; + for (const item of this.listeners) { + if (this.map.available_ids.indexOf(item.id) >= 0) { + const selector = `#${cleanCssSelector(item.id)}`; + const element = this.map_element.nativeElement.querySelector(selector); + if (element) { + this._event_handlers.push( + this._renderer.listen(element, item.event, item.callback) + ); + continue; + } + } + log('LISTEN', `Update to listen to "${item.event}" on element "${item.id}"`); + } + } +} diff --git a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.html b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.html index 00075db..a8961a2 100644 --- a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.html +++ b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.html @@ -1,20 +1,20 @@ -<div class="map-overlay-outlet" widget *ngIf="items"> - <div [class]="'map-poi' + (item.type ? ' ' + item.type : '')" widget *ngFor="let item of items; trackBy: trackByFn"> +<ng-container *ngIf="items && items.length"> + <ng-container *ngFor="let item of items; trackBy:trackByFn"> <div - class="item" - *ngIf="item.position && (item.position.x || item.position.y)" - [style.top]="item.position.y + '%'" - [style.left]="item.position.x + '%'" + *ngIf="item.coordinates" + class="map-overlay" + [style.top]="(item.coordinates.y * 100) + '%'" + [style.left]="(item.coordinates.x * 100) + '%'" + [style.transform]="'rotate(' + rotation + 'deg)'" + [ngSwitch]="item.content_type" > - <ng-container [ngSwitch]="item.method"> - <div *ngSwitchCase="'text'" [innerHTML]="item.content"></div> - <ng-container *ngSwitchCase="'template'"> - <ng-container *ngTemplateOutlet="item.content; context: item.context"></ng-container> - </ng-container> - <ng-container *ngSwitchCase="'component'"> - <ng-container *ngComponentOutlet="item.content; injector: item.injector"></ng-container> - </ng-container> + <ng-container *ngSwitchCase="'component'"> + <ng-container *ngComponentOutlet="item.content; injector: (injectors || {})[item.id]"></ng-container> </ng-container> + <ng-container *ngSwitchCase="'template'"> + <ng-container *ngTemplateOutlet="item.content"></ng-container> + </ng-container> + <div *ngSwitchDefault [innerHTML]="item.content"></div> </div> - </div> -</div> + </ng-container> +</ng-container> diff --git a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.scss b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.scss index 48bbad7..b89fec0 100644 --- a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.scss +++ b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.scss @@ -1,20 +1,6 @@ - -.map-overlay-outlet { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - pointer-events: none; -} - -.item { +.map-overlay { position: absolute; - top: -1000vh; - left: -1000vw; height: 1px; width: 1px; - pointer-events: auto; } diff --git a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.spec.ts b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.spec.ts new file mode 100644 index 0000000..55be766 --- /dev/null +++ b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MapOverlayOutletComponent } from './map-overlay-outlet.component'; + +describe('MapOverlayOutletComponent', () => { + let component: MapOverlayOutletComponent; + let fixture: ComponentFixture<MapOverlayOutletComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MapOverlayOutletComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MapOverlayOutletComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.ts b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.ts index ad2c992..8ca5202 100644 --- a/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.ts +++ b/projects/library/src/lib/components/map-overlay-outlet/map-overlay-outlet.component.ts @@ -1,105 +1,64 @@ -import { - Component, - Input, - Output, - EventEmitter, - SimpleChanges, - Renderer2, - OnInit, - OnChanges, - TemplateRef, - Injector, - Injectable -} from '@angular/core'; - -import { BaseWidgetDirective } from '../../base.directive'; - -import { MapUtilities } from '../../utlities/map.utilities'; -import { AMapFeature } from '../map-feature/map-feature.class'; - +import { Component, Input, Injector, SimpleChanges, OnChanges, Type } from '@angular/core'; +import { MapRenderFeature } from '../../classes/map-render-feature'; +import { HashMap, Point } from '../../helpers/type.helpers'; +import { MapState, MAP_STATE, MAP_LOCATION, MAP_OVERLAY_DATA } from '../../helpers/map.interfaces'; +import { BehaviorSubject } from 'rxjs'; @Component({ selector: 'map-overlay-outlet', templateUrl: './map-overlay-outlet.component.html', styleUrls: ['./map-overlay-outlet.component.scss'] }) -export class MapOverlayOutletComponent extends BaseWidgetDirective implements OnInit, OnChanges { - /** List of points of interest */ - @Input() items: AMapFeature[]; - /** Map elment render to the DOM */ - @Input() map: SVGElement; - /** Map root element */ - @Input() container: HTMLDivElement; - /** Zoom level as decimal */ - @Input() scale: number; - /** Event emitter for Point of interest events */ - @Output() event = new EventEmitter(); +export class MapOverlayOutletComponent implements OnChanges { + /** List of text items to render on top of the map */ + @Input() items: MapRenderFeature[] = []; + /** Rotation of the map */ + @Input() zoom = 1; + /** Rotation of the map */ + @Input() center: Point = { x: .5, y: .5 }; + /** Rotation of the map */ + @Input() rotation = 0; + /** List of injectors for overlay items */ + public injectors: HashMap<Injector> = {}; + /** Emitter for changes to the state of the map */ + private _state: BehaviorSubject<MapState> = new BehaviorSubject({ zoom: 1, center: { x: 0.5, y: 0.5 } }); - protected list: AMapFeature[] = []; + constructor(private _injector: Injector) {} - constructor(private injector: Injector, protected renderer: Renderer2) { - super(); - } - - public ngOnInit() { - if (this.isIE()) { - this.renderer.listen('window', 'resize', () => { - this.timeout('update', () => this.processItems(), 200); - }); + public ngOnChanges(changes: SimpleChanges): void { + if (changes.items && this.items) { + delete this.injectors; + this.injectors = {}; + for (const item of this.items) { + if (item.content instanceof Type) { + this.injectors[item.id] = this._createInjector(item); + } + } } - } - - public ngOnChanges(changes: SimpleChanges) { - if (changes.items || changes.map) { - this.timeout('update', () => this.processItems(), changes.map && !changes.map.previousValue ? 1000 : 200); + if (changes.zoom || changes.center) { + this._state.next({ + zoom: this.zoom || 1, + center: this.center || { x: 0.5, y: 0.5 } + }); } } - public processItems() { - if (!this.items) { return; } - if (this.items.length <= 0) { this.list = []; return; } - this.timeout('process', () => { - this.list = [...this.items]; - this.updateItems(); - }); - } - - public updateItems() { - if (this.map) { - const view_box = this.map.getAttribute('viewBox').split(' '); - const map_box = this.map.getBoundingClientRect(); - const box = this.container.getBoundingClientRect(); - const x_scale = Math.max(1, (map_box.width / map_box.height) / (+view_box[2] / +view_box[3])); - const y_scale = Math.max(1, (map_box.height / map_box.width) / (+view_box[3] / +view_box[2])); - for (const feature of this.list) { - this.calculatePosition(feature, { x_scale, y_scale, view: view_box, map: map_box, cntr: box }); - } - } + public trackByFn(item: MapRenderFeature, index: number) { + return item.id || JSON.stringify(item.coordinates) || index; } /** - * Calculate render position of the given point of interest - * @param item POI Item + * Create injector for overlay element + * @param item Feature needing a injector */ - public calculatePosition(item: AMapFeature, details: { [name: string]: any }) { - const el = item.id ? this.map.querySelector(MapUtilities.cleanCssSelector(`#${item.id}`)) : null; - if (el || item.coordinates) { - const pos_box = this.isIE() && item.coordinates ? { width: +details.view[2], height: +details.view[3] } : details.cntr; - const position = MapUtilities.getPosition(pos_box, el, item.coordinates) || { x: .5, y: .5 }; - if (this.isIE() && item.coordinates) { - // Normalise dimensions - position.x = position.x / details.x_scale + (details.x_scale - 1) / 2; - position.y = position.y / details.y_scale + (details.y_scale - 1) / 2; - } - item.position = position; - } - } - - public isIE() { - return navigator.appName == 'Microsoft Internet Explorer' || !!(navigator.userAgent.match(/Trident/) || navigator.userAgent.match(/rv:11/)) || !!navigator.userAgent.match(/MSIE/g); - } - - public trackByFn(index: number, item: AMapFeature) { - return item ? item.id || `${item.position.x},${item.position.y}` : index; + private _createInjector(item: MapRenderFeature) { + return Injector.create({ + providers: [ + { provide: MAP_STATE, useValue: this._state }, + { provide: MAP_LOCATION, useValue: item.coordinates }, + { provide: MAP_OVERLAY_DATA, useValue: item.data } + ], + parent: this._injector + }); } } diff --git a/projects/library/src/lib/components/map-renderer/map-renderer.component.ts b/projects/library/src/lib/components/map-renderer/map-renderer.component.ts deleted file mode 100644 index 55959f0..0000000 --- a/projects/library/src/lib/components/map-renderer/map-renderer.component.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { Component, Input, ElementRef, ViewChild, Output, EventEmitter, Renderer2, SimpleChanges, OnInit, OnChanges } from '@angular/core'; -import { MapUtilities } from '../../utlities/map.utilities'; -import { MapService } from '../../services/map.service'; -import { BaseWidgetDirective } from '../../base.directive'; -import { IMapPoint } from '../map.interfaces'; -import { AMapFeature } from '../map-feature/map-feature.class'; - -@Component({ - selector: 'aca-map-renderer', - templateUrl: `./map-renderer.template.html`, - styleUrls: [`./map-renderer.styles.scss`] -}) -export class MapRendererComponent extends BaseWidgetDirective implements OnInit, OnChanges { - /** Zoom level of the map */ - @Input() public scale = 1; - /** Point within the map to center in the view */ - @Input() public center: IMapPoint = { x: .5, y: .5 }; - /** URL of the Map SVG to render */ - @Input() public src = ''; - /** CSS styles to apply to the map */ - @Input() public css = ''; - /** Re-renders the map on changes to this */ - @Input() public redraw: any = null; - /** List of points of interest */ - @Input() public items: AMapFeature[]; - /** Change emitter for SVG DOM element */ - @Output() public map = new EventEmitter<SVGElement>(); - - /** Block to render SVG element */ - @ViewChild('renderBlock', { static: true }) public render_block: ElementRef<HTMLDivElement>; - /** Canvas to render static images of map when zooming */ - @ViewChild('canvas', { static: true }) private canvas: ElementRef<HTMLCanvasElement>; - @ViewChild('content', { static: true }) private content: ElementRef<HTMLDivElement>; - @ViewChild('container', { static: true }) private container: ElementRef<HTMLDivElement>; - - public model: { [name: string]: any } = {}; - - constructor(private service: MapService, private renderer: Renderer2, private el: ElementRef) { - super(); - this.model.is_IE = navigator.appName == 'Microsoft Internet Explorer' || !!(navigator.userAgent.match(/Trident/) || navigator.userAgent.match(/rv:11/)) || !!navigator.userAgent.match(/MSIE/g); - } - - public ngOnInit() { - this.subs.obs.resize = this.renderer.listen('window', 'resize', () => this.resize()); - this.model.center = { x: .5, y: .5 }; - this.model.position = { x: -50, y: -50 }; - this.model.loading = true; - if (this.el) { this.renderer.setAttribute(this.el.nativeElement, `map-${this.id}`, 'true') } - } - - public ngOnChanges(changes: SimpleChanges) { - super.ngOnChanges(changes); - if (changes.scale || changes.center) { - this.update(); - } - if (changes.src) { - this.loadMap(); - } - if (changes.redraw || changes.css) { - this.renderImage(); - } - } - - public get isIE() { - return this.model.is_IE; - } - - /** - * Update display of map - */ - public update() { - this.timeout('update', () => { - this.model.zooming = false; - this.model.panning = false; - this.updateZoom(); - this.updatePosition(); - }, 5); - } - - /** - * Update the drawn zoom level of the map - */ - public updateZoom() { - const scale = (this.scale || 1) * 100; - if (!this.model.zoom) { this.model.zoom = scale; } - if (this.model.zoom !== scale) { - this.model.zooming = true; - const dir = scale - this.model.zoom < 0 ? -1 : 1; - this.model.zoom += Math.min(Math.max(.1, Math.abs(scale - this.model.zoom) / 10), 50) * dir; - if (Math.abs(scale - this.model.zoom) < .5) { this.model.zoom = scale } - this.update(); - } - } - - /** - * Update the drawn position of the map - */ - public updatePosition() { - const center = this.center || { x: .5, y: .5}; - if (!this.model.center) { this.model.center = center; } - if (this.model.center.x !== center.x) { - this.model.panning = true; - const dir = center.x - this.model.center.x < 0 ? -1 : 1; - this.model.center.x += Math.min(Math.max(.0001, Math.abs(center.x - this.model.center.x) / 5), .1) * dir; - if (Math.abs(center.x - this.model.center.x) < .005) { this.model.center.x = center.x } - this.update(); - } - if (this.model.center.y !== center.y) { - this.model.panning = true; - const dir = center.y - this.model.center.y < 0 ? -1 : 1; - this.model.center.y += Math.min(Math.max(.0001, Math.abs(center.y - this.model.center.y) / 5), .1) * dir; - if (Math.abs(center.y - this.model.center.y) < .005) { this.model.center.y = center.y } - this.update(); - } - // Generate draw position - this.model.position = { - x: -50 -((this.model.center.x - .5) * 100), - y: -50 -((this.model.center.y - .5) * 100) - }; - } - - /** - * Load map data from SVG file - */ - private loadMap() { - this.model.loading = true; - this.map.emit(null); - this.model.map_data = ''; - this.service.loadMap(this.src).then((data) => { - this.model.map_data = data; - this.timeout('load', () => { - if (this.content) { - this.model.loading = true; - this.model.map = this.content.nativeElement.querySelector('svg'); - this.renderImage(); - this.timeout('resize', () => this.resize()); - if (this.model.map) { - this.renderer.setAttribute(this.model.map, 'preserveAspectRatio', 'xMidYMid meet'); - this.renderer.setStyle(this.model.map, 'width', '100%'); - } - this.map.emit(this.model.map); - } - }); - }, () => this.service.log('Error', `Unable to load map '${this.src}'`)); - } - - /** - * Render SVG map to an image to draw while zooming - */ - private renderImage() { - this.timeout('render', () => { - if (!this.content || !this.model.map) { - return this.renderImage(); - } - let box = this.model.map.getBoundingClientRect(); - let width = window.devicePixelRatio * window.innerWidth; - let ratio = (box.width / box.height) || 1; - if (this.model.img) { delete this.model.img; } - this.model.img = document.createElement('img'); - const canvas = this.canvas.nativeElement; - const context = canvas.getContext('2d'); - this.model.img.onerror = (err) => console.log(err); - this.model.img.onload = () => { - canvas.width = width; - canvas.height = width / ratio; - context.drawImage(this.model.img, 0, 0, canvas.width, canvas.height); - }; - const data_with_styles = (this.model.map_data || '').replace(`</style>`, `${this.css}</style>`) - .replace('<svg', `<svg width="${width}px" height="${Math.floor(width / ratio)}px" preserveAspectRatio="xMidYMid meet"`); - const base64_data = MapUtilities.base64Encode(data_with_styles); - this.model.img.src = `data:image/svg+xml;base64,${base64_data}`; - - }); - } - - /** - * Scale map to fit in the the parent container - */ - private resize() { - if (this.container && this.model.map) { - const box = this.container.nativeElement.getBoundingClientRect(); - const map_box = this.model.map.getBoundingClientRect(); - // Check that map is rendered - if (map_box.height === 0 || map_box.width === 0) { - return this.timeout('resize_fail', () => this.resize()); - } - const box_ratio = box.width / box.height; - const map_ratio = map_box.width / map_box.height; - this.model.ratio = Math.min(map_ratio / box_ratio, 1); - this.model.loading = false; - } else { - if (!this.model.map) { - this.model.map = this.content.nativeElement.querySelector('svg'); - } - this.timeout('resize_fail', () => this.resize()); - } - } -} diff --git a/projects/library/src/lib/components/map-renderer/map-renderer.styles.scss b/projects/library/src/lib/components/map-renderer/map-renderer.styles.scss deleted file mode 100644 index 1de492a..0000000 --- a/projects/library/src/lib/components/map-renderer/map-renderer.styles.scss +++ /dev/null @@ -1,86 +0,0 @@ - -.container { - position: absolute; - top: 50%; - left: 50%; - height: calc(100% - 2em); - width: calc(100% - 2em); - pointer-events: none; - transform: translate(-50%, -50%); -} - -.map { - position: absolute; - height: 100%; - width: 100%; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - transition: opacity 200ms; - will-change: width, height; -} - -.render-block { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - top: 50%; - left: 50%; - height: 100%; - width: 100%; - will-change: transform; -} - -.blk { - position: relative; - flex: 1; - display: flex; - width: 100%; - height: 100%; -} - -.map-block { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1; - - &.img { - z-index: 0; - } - - img { - height: 100%; - width: 100%; - pointer-events: none; - z-index: 0; - } -} - -.loader { - position: absolute; - top: 50%; - left: 50%; - border-radius: 4px; - padding: 1em; - font-size: .8em; - background-color: #fff; - box-shadow: 0 1px 3px 0 rgba(#000,.2), 0 1px 1px 0 rgba(#000,.14), 0 2px 1px -1px rgba(#000,.12); - transform: translate(-50%, -50%); -} - -canvas { - position: absolute; - margin: auto; - width: 100%; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 1; -} diff --git a/projects/library/src/lib/components/map-renderer/map-renderer.template.html b/projects/library/src/lib/components/map-renderer/map-renderer.template.html deleted file mode 100644 index f2bfa8d..0000000 --- a/projects/library/src/lib/components/map-renderer/map-renderer.template.html +++ /dev/null @@ -1,18 +0,0 @@ -<div class="container" #container> - <div class="map" #content [style.width]="((model.ratio || 1) * 100) + '%'" [style.opacity]="model.loading || !model.map_data ? 0 : 1"> - <div class="render-block" #renderBlock [style.width]="model.zoom + '%'" [style.height]="model.zoom + '%'" - [style.transform]="'translate(' + model.position.x + '%, ' + model.position.y + '%)'"> - <div class="blk"> - <div class="map-block" [style.opacity]="isIE || model.zooming ? 0 : 1" [innerHTML]="model.map_data | safe:'html'"></div> - <div class="map-block img" *ngIf="isIE" [style.opacity]="model.zooming ? 0 : 1"> - <img *ngIf="model.img && model.img.src" [src]="model.img.src | safe:'resource'"> - </div> - <canvas #canvas [style.opacity]="model.zooming ? 1 : 0"></canvas> - <map-overlay-outlet [items]="items" [container]="render_block?.nativeElement" [scale]="model.zoom / 100" [map]="model.map"></map-overlay-outlet> - </div> - </div> - </div> - <div class="loader" [style.display]="model.loading || !model.map_data ? '' : 'none'"> - <ng-content></ng-content> - </div> -</div> \ No newline at end of file diff --git a/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.html b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.html new file mode 100644 index 0000000..50064fc --- /dev/null +++ b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.html @@ -0,0 +1,18 @@ +<ng-container *ngIf="items && items.length"> + <ng-container *ngFor="let item of items"> + <div + *ngIf="item.coordinates && (!item.show_after_zoom || zoom >= item.show_after_zoom)" + class="map-text" + [style.top]="item.coordinates.y * 100 + '%'" + [style.left]="item.coordinates.x * 100 + '%'" + [style.transform]="'rotate(' + rotation + 'deg)'" + > + <div + class="text" + [ngStyle]="item.data" + > + {{ item.content }} + </div> + </div> + </ng-container> +</ng-container> diff --git a/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.scss b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.scss new file mode 100644 index 0000000..94c11fb --- /dev/null +++ b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.scss @@ -0,0 +1,19 @@ + +.map-text { + position: absolute; + font-size: 1rem; +} + +.text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #fff; + font-size: .8em; + text-shadow: 0 0 2px #000; + white-space: pre-line; + min-width: 20em; + text-align: center; + pointer-events: none; +} diff --git a/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.spec.ts b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.spec.ts new file mode 100644 index 0000000..5f71846 --- /dev/null +++ b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.spec.ts @@ -0,0 +1,24 @@ +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +// import { MapTextOutletComponent } from './map-text-outlet.component'; + +// describe('MapTextOutletComponent', () => { +// let component: MapTextOutletComponent; +// let fixture: ComponentFixture<MapTextOutletComponent>; + +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [MapTextOutletComponent] +// }).compileComponents(); +// })); + +// beforeEach(() => { +// fixture = TestBed.createComponent(MapTextOutletComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// }); + +// it('should create', () => { +// expect(component).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.ts b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.ts new file mode 100644 index 0000000..8136511 --- /dev/null +++ b/projects/library/src/lib/components/map-text-outlet/map-text-outlet.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; + +import { MapTextFeature } from '../../helpers/map.interfaces'; + +@Component({ + selector: 'map-text-outlet', + templateUrl: './map-text-outlet.component.html', + styleUrls: ['./map-text-outlet.component.scss'] +}) +export class MapTextOutletComponent { + /** List of text items to render on top of the map */ + @Input() items: MapTextFeature[] = []; + /** Rotation of the map */ + @Input() zoom: number = 1; + /** Rotation of the map */ + @Input() rotation: number = 0; +} diff --git a/projects/library/src/lib/components/map.interfaces.ts b/projects/library/src/lib/components/map.interfaces.ts deleted file mode 100644 index 34d43f0..0000000 --- a/projects/library/src/lib/components/map.interfaces.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { TemplateRef, Type } from '@angular/core'; - -/** Point of interest */ -export interface IMapFeature<T = any> { - /** Map Element selector */ - id?: string; - /** Map coordinates */ - coordinates?: IMapPoint; - /** Content to render at position */ - content: MapOverlayContent; - /** Content render method. Determined automatically */ - method?: string; - /** Data to inject into the content template/component */ - data?: T; - /** Default zoom level for focus items */ - zoom?: number; -} - -/** A coordinate */ -export interface IMapPoint { - /** x coordinate */ - x: number; - /** y coordinate */ - y: number; -} - -/** A coordinate with readonly x and y values */ -export interface IReadonlyMapPoint { - /** x coordinate */ - readonly x: number; - /** y coordinate */ - readonly y: number; -} - -/** CSS selector to style mappings */ -export interface IStyleMappings { - [selector: string]: { - [property: string]: (number | string) - } -} - -/** Valid content types Template, Component or HTML string */ -export type MapOverlayContent = TemplateRef<any> | Type<any> | string; diff --git a/projects/library/src/lib/components/map/map.component.html b/projects/library/src/lib/components/map/map.component.html new file mode 100644 index 0000000..d0a0ca3 --- /dev/null +++ b/projects/library/src/lib/components/map/map.component.html @@ -0,0 +1,12 @@ +<a-map-outlet + [map]="map" + [(zoom)]="zoom" + (zoomChange)="zoomChange.emit($event)" + [(center)]="center" + (centerChange)="centerChange.emit($event)" + (events)="events.emit($event)" + [features]="feature_list" + [listeners]="listeners" + [text]="text_list" + [class.locked]="options?.lock" +></a-map-outlet> diff --git a/projects/library/src/lib/components/map/map.component.scss b/projects/library/src/lib/components/map/map.component.scss new file mode 100644 index 0000000..bbcf187 --- /dev/null +++ b/projects/library/src/lib/components/map/map.component.scss @@ -0,0 +1,13 @@ + +:host { + position: relative; + height: 100%; + width: 100%; + overflow: hidden; +} + +a-map-outlet { + &.locked { + pointer-events: none; + } +} diff --git a/projects/library/src/lib/components/map/map.component.spec.ts b/projects/library/src/lib/components/map/map.component.spec.ts index ffd6838..376ef68 100644 --- a/projects/library/src/lib/components/map/map.component.spec.ts +++ b/projects/library/src/lib/components/map/map.component.spec.ts @@ -1,51 +1,40 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component } from '@angular/core'; -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { CommonModule } from '@angular/common'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - +import { MapComponent } from './map.component'; import { MapService } from '../../services/map.service'; -import { AMapComponent } from './map.component'; -import { MapOverlayOutletComponent } from '../map-overlay-outlet/map-overlay-outlet.component'; -import { MapRendererComponent } from '../map-renderer/map-renderer.component'; -import { MapInputDirective } from '../../directives/map-input.directive'; -import { MapStylerDirective } from '../../directives/map-styler.directive'; -import { APipesModule } from '@acaprojects/ngx-pipes'; -import { HttpClientModule } from '@angular/common/http'; +import { RenderableMap } from '../../classes/renderable-map'; -describe('AMapComponent', () => { - let fixture: ComponentFixture<AMapComponent>; - let component: AMapComponent; - let service: MapService - let clock: jasmine.Clock; +@Component({ + selector: 'a-map-outlet', + template: '', + inputs: ['zoom', 'center', 'listeners', 'features', 'text', 'map'] +}) +class MockMapOutlet {} - beforeEach(() => { +describe('MapComponent', () => { + let component: MapComponent; + let fixture: ComponentFixture<MapComponent>; + let service: any; + + beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ - AMapComponent, - MapOverlayOutletComponent, - MapRendererComponent, - MapInputDirective, - MapStylerDirective - ], + declarations: [MapComponent, MockMapOutlet], providers: [ - MapService - ], - imports: [CommonModule, HttpClientModule, APipesModule, NoopAnimationsModule] + { provide: MapService, useValue: jasmine.createSpyObj('MapService', ['loadMap']) } + ] }).compileComponents(); - fixture = TestBed.createComponent(AMapComponent); - component = fixture.debugElement.componentInstance; + })); + + beforeEach(() => { service = TestBed.get(MapService); - clock = jasmine.clock(); - clock.uninstall(); - clock.install(); + service.loadMap.and.returnValue(Promise.resolve(new RenderableMap('test.svg', '<svg></svg>'))); + fixture = TestBed.createComponent(MapComponent); + component = fixture.componentInstance; fixture.detectChanges(); }); - afterEach(() => { - clock.uninstall(); - }); - - it('should create an instance', () => { + it('should create', () => { expect(component).toBeTruthy(); }); }); diff --git a/projects/library/src/lib/components/map/map.component.ts b/projects/library/src/lib/components/map/map.component.ts index dff621d..7bcfb02 100644 --- a/projects/library/src/lib/components/map/map.component.ts +++ b/projects/library/src/lib/components/map/map.component.ts @@ -1,126 +1,132 @@ +import { Component, OnInit, Input, OnChanges, SimpleChanges, OnDestroy, Output, EventEmitter, Renderer2 } from '@angular/core'; -import { Component, Input, OnChanges, Output, EventEmitter, SimpleChanges, Injector } from '@angular/core'; - -import { IMapListener } from '../../directives/map-styler.directive'; -import { BaseWidgetDirective } from '../../base.directive'; -import { IMapFeature, IMapPoint, IStyleMappings } from '../map.interfaces'; -import { AMapFeature } from '../map-feature/map-feature.class'; +import { Point, HashMap, MapOptions, MapEvent } from '../../helpers/type.helpers'; +import { MapRenderFeature } from '../../classes/map-render-feature'; +import { MapService } from '../../services/map.service'; +import { RenderableMap } from '../../classes/renderable-map'; +import { MapStyles } from '../../classes/map-styles'; +import { log } from '../../settings'; +import { MapFeature, MapTextFeature, MapListener } from '../../helpers/map.interfaces'; @Component({ - selector: 'a-map', - templateUrl: './map.template.html', - styles: [` - .container { - position: relative; - width: 100%; - height: 100%; - min-height: 12em; - min-width: 12em; - overflow: hidden; - z-index: 1; - } - i { display: none } - `] + selector: 'a-map', + templateUrl: './map.component.html', + styleUrls: ['./map.component.scss'] }) -export class AMapComponent extends BaseWidgetDirective implements OnChanges { - /** URL to the map SVG file */ +export class MapComponent implements OnInit, OnChanges, OnDestroy { + /** URL to the map resource to be displayed */ @Input() public src: string; - /** Mapping of CSS styles to apply to the map */ - @Input('cssStyles') public style_map: IStyleMappings; - /** Zoom level as a percentage */ + /** Zoom level of the map as a whole number. 1 = 100% zoom */ @Input() public zoom: number; - /** Points of interest to render on the map */ - @Input() public features: IMapFeature[]; - /** Point on map to center on */ - @Input() public focus: IMapFeature; - /** Event listeners for elements on the map */ - @Input() public listeners: IMapListener[]; - /** Center point of the map */ - @Input() public center: IMapPoint; - /** Disable moving and zooming the map */ - @Input() public lock: boolean; - /** Emitter for changes to the zoom percentage */ - @Output() public zoomChange = new EventEmitter(); - /** Emitter for changes to the center position */ - @Output() public centerChange = new EventEmitter(); - /** Emitter for listened events */ - @Output() public event = new EventEmitter(); + /** + * Position of the center point of the component on the map displayed + * + * For example: + * + * { x: 0, y: 0 } + * Places the map top left corner in the middle of the component + * + * { x: 0.5, y: 0.5 } + * Places the center of the map in the middle of the component + * + * { x: 1, y: 1 } + * Places the bottom right corner of the map in the middle of the component + */ + @Input() public center: Point = { x: .5, y: .5 }; + /** List of elements to render on top of the map */ + @Input() public features: MapFeature[] = []; + /** List of elements to render on top of the map */ + @Input() public text: MapTextFeature[] = []; + /** List of listeners for elements on the map */ + @Input() public listeners: MapListener[] = []; + /** Mapping of CSS selectors to override styles */ + @Input() public css: HashMap<HashMap<string>> = {}; + /** Element or Point to focus the map on */ + @Input() public focus: string | Point; + /** Optional flags for the map */ + @Input() public options: MapOptions; + /** Emitter for changes to the zoom value */ + @Output() public zoomChange = new EventEmitter<number>(); + /** Emitter for changes to the center value */ + @Output() public centerChange = new EventEmitter<Point>(); + /** Emitter for changes to the zoom value */ + @Output() public events = new EventEmitter<MapEvent>(); + /** Details of the currently displayed map */ + public map: RenderableMap; + /** */ + public styler: MapStyles; + /** List of elements to render on top of the map */ + public feature_list: MapRenderFeature[] = []; + /** List of text to render on top of the map */ + public text_list: MapRenderFeature[] = []; - /** SVG Element for the map */ - public map: SVGElement; - /** List of processed map features */ - public render_features: AMapFeature[] = []; - /** String of mapped CSS values */ - public css: string; - /** Processed zoom level value */ - public scale: number; + constructor(private _service: MapService) { } + + public ngOnInit() { - constructor(private _injector: Injector) { - super(); } public ngOnChanges(changes: SimpleChanges) { - super.ngOnChanges(changes); + if (changes.src && this.src) { + this._service.loadMap(this.src).then((map) => { + this.map = map; + this.updateStyles(); + this.feature_list = this.processFeatures(this.features || []); + this.text_list = this.processFeatures(this.text || []); + }); + } + if (changes.css) { + this.updateStyles(); + } + if (changes.focus && this.focus) { + this.onFocusChange(this.focus); + } if (changes.features) { - this.updateFeatures(); + this.feature_list = this.processFeatures(this.features || []); } - if (changes.zoom) { - this.scale = this.zoom; + if (changes.text) { + this.text_list = this.processFeatures(this.text || []); } } - /** - * Update the center position of the map and emit the change - * @param center New center position - */ - public postCenter(center: IMapPoint): void { - this.center = center; - this.centerChange.emit(center); - } - - /** - * Update the zoom level of the map and emit the change - * @param zoom New zoom level - */ - public postZoom(zoom: number): void { - this.zoom = zoom; - this.zoomChange.emit(zoom); - } - - public handleEvent(e) { - - } - - public updateMap(el: SVGElement) { - this.timeout('map', () => this.map = el, 10); + public ngOnDestroy(): void { + if (this.styler) { + this.styler.destroy(); + this.styler = null; + } } - /** - * Reset the position and scale to their intial values - */ - public reset() { - this.postZoom(1); - this.scale = 1; - this.postCenter({ x: .5, y: .5 }); + public updateStyles() { + if (this.styler) { + this.styler.destroy(); + } + this.styler = new MapStyles(this.css || {}, this.map); } /** - * Update the list of features keeping already existing feature + * Update focused point or element + * @param location ID of the element to focus or a point within the map */ - private updateFeatures() { - const features = this.features; - this.render_features = (features || []).reduce((a, v) => { - const feature = this.render_features.find( - i => i.id === v.id && i.content === v.content - ); - if (feature) { - feature.data = v.data; - a.push(feature); + public onFocusChange(location: string | Point) { + if (!this.map) { return; } + if (typeof location === 'string') { + const element = this.map.element_map[location]; + if (!element) { + log('MAP', `No element for id "${location}"`, undefined, 'warn'); + return; } else { - a.push(new AMapFeature(this, this._injector, v)); + this.center = element.coordinates; } - return a; - }, []); + } else { + this.center = { + x: Math.max(0, Math.min(1, location.x || this.center.x)), + y: Math.max(0, Math.min(1, location.y || this.center.y)) + }; + } } + public processFeatures(list: MapFeature[]): MapRenderFeature[] { + if (!this.map) { return []; } + return list.map(i => new MapRenderFeature(i, this.map)); + } } diff --git a/projects/library/src/lib/components/map/map.template.html b/projects/library/src/lib/components/map/map.template.html deleted file mode 100644 index 40e2b78..0000000 --- a/projects/library/src/lib/components/map/map.template.html +++ /dev/null @@ -1,30 +0,0 @@ -<div - class="map container" - widget - aca-map-input - [id]="id" - [(scale)]="scale" - (scaleChange)="postZoom($event)" - [(center)]="center" - (centerChange)="postCenter($event)" - [listeners]="listeners" - (event)="handleEvent($event)" - [map]="map" - [focus]="focus" - [lock]="lock" - [src]="src" - (dblclick)="reset()" -> - <i aca-map-styler [id]="id" [cssStyles]="style_map" (css)="css = $event"></i> - <aca-map-renderer - [id]="id" - [src]="src" - [scale]="scale" - [center]="center" - (map)="updateMap($event)" - [css]="css" - [items]="render_features" - > - <ng-content></ng-content> - </aca-map-renderer> -</div> diff --git a/projects/library/src/lib/components/overlays/map-pin/map-pin.component.html b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.html new file mode 100644 index 0000000..b60209a --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.html @@ -0,0 +1,26 @@ + +<div class="pin" [@show]> + <svg + version="1.1" + id="Layer_1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + viewBox="0 0 380 560" + enable-background="new 0 0 380 560" + xml:space="preserve" + > + <g> + <path + [style.fill]="fill" + [style.stroke]="stroke" + stroke-width="25" + d="M182.9,551.7c0,0.1,0.2,0.3,0.2,0.3S358.3,283,358.3,194.6c0-130.1-88.8-186.7-175.4-186.9 + C96.3,7.9,7.5,64.5,7.5,194.6c0,88.4,175.3,357.4,175.3,357.4S182.9,551.7,182.9,551.7z M122.2,187.2c0-33.6,27.2-60.8,60.8-60.8 + c33.6,0,60.8,27.2,60.8,60.8S216.5,248,182.9,248C149.4,248,122.2,220.8,122.2,187.2z" + /> + </g> + </svg> + <div class="text" *ngIf="text" [@showText]>{{ text }}</div> +</div> diff --git a/projects/library/src/lib/components/overlays/map-pin/map-pin.component.scss b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.scss new file mode 100644 index 0000000..455bcea --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.scss @@ -0,0 +1,23 @@ + +.pin { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 2rem; +} + +.text { + position: absolute; + top: -.5em; + left: 50%; + transform: translate(-50%, -100%); + background-color: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px 0 rgba(#000, .2), + 0 1px 1px 0 rgba(#000, .14), + 0 2px 1px -1px rgba(#000, .12); + white-space: nowrap; + font-size: .8em; + padding: .5em .75em; +} diff --git a/projects/library/src/lib/components/overlays/map-pin/map-pin.component.spec.ts b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.spec.ts new file mode 100644 index 0000000..f6c9dc3 --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.spec.ts @@ -0,0 +1,48 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { MapPinComponent } from './map-pin.component'; +import { MAP_OVERLAY_DATA } from '../../../helpers/map.interfaces'; + +describe('MapPinComponent', () => { + let component: MapPinComponent; + let fixture: ComponentFixture<MapPinComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MapPinComponent], + providers: [ + { provide: MAP_OVERLAY_DATA, useValue: {} } + ], + imports: [NoopAnimationsModule] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MapPinComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show a pin', () => { + const compiled: HTMLElement = fixture.debugElement.nativeElement; + const element: SVGElement = compiled.querySelector('svg'); + expect(element).toBeTruthy(); + }); + + it('should show text', () => { + const compiled: HTMLElement = fixture.debugElement.nativeElement; + let element: HTMLDivElement = compiled.querySelector('.text'); + expect(element).toBeFalsy(); + (component as any)._data.text = 'Test'; + component.show_text = true; + fixture.detectChanges(); + element = compiled.querySelector('.text'); + expect(element).toBeTruthy(); + expect(element.textContent).toBe('Test'); + }); +}); diff --git a/projects/library/src/lib/components/overlays/map-pin/map-pin.component.ts b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.ts index d010848..5cf1777 100644 --- a/projects/library/src/lib/components/overlays/map-pin/map-pin.component.ts +++ b/projects/library/src/lib/components/overlays/map-pin/map-pin.component.ts @@ -1,37 +1,55 @@ - -import { Component } from '@angular/core'; +import { Component, OnInit, Inject } from '@angular/core'; import { animate, style, transition, trigger } from '@angular/animations'; -import { AMapFeature } from '../../map-feature/map-feature.class'; +import { MAP_OVERLAY_DATA } from '../../../helpers/map.interfaces'; + +export interface MapPinData { + /** Text to render above the pin */ + readonly text: string; + /** CSS colour of the pin outline */ + readonly stroke: string; + /** CSS colour of the pin */ + readonly fill: string; +} @Component({ - selector: 'map-pin', - templateUrl: './map-pin.template.html', - styleUrls: ['./map-pin.styles.scss'], + selector: 'a-map-pin', + templateUrl: './map-pin.component.html', + styleUrls: ['./map-pin.component.scss'], animations: [ trigger('show', [ transition(':enter', [ style({ transform: 'translate(-50%, -100%)', opacity: 0 }), - animate(300, style({ transform: 'translate(-50%, 0%)', opacity: 1 })), + animate(200, style({ transform: 'translate(-50%, 0%)', opacity: 1 })), ]), - transition(':leave', [style({ opacity: 1 }), animate(300, style({ opacity: 0 }))]), + transition(':leave', [style({ opacity: 1 }), animate(200, style({ opacity: 0 }))]), + ]), + trigger('showText', [ + transition(':enter', [ + style({ transform: 'translate(-50%, -200%)', opacity: 0 }), + animate(200, style({ transform: 'translate(-50%, -100%)', opacity: 1 })), + ]), + transition(':leave', [style({ opacity: 1 }), animate(200, style({ opacity: 0 }))]), ]), ], }) export class MapPinComponent { - /** Contextual data associated with the component */ - public context: AMapFeature; - /** Display text above the pin */ - public text: string; - /** Primary colour of the pin */ - public back: string; - /** Secondary colour of the pin */ - public fore: string; - - constructor(context: AMapFeature) { - this.context = context || {} as any; - this.text = this.context.data.text || ''; - this.back = this.context.data.back || '#FFFFFF'; - this.fore = this.context.data.fore || '#DC6900'; + + public show_text: boolean = false; + + public get text(): string { + return this.show_text ? (this._data ? this._data.text : '') : ''; + } + + public get stroke(): string { + return (this._data ? this._data.stroke : '') || '#fff'; + } + + public get fill(): string { + return (this._data ? this._data.fill : '') || '#f44336'; + } + + constructor(@Inject(MAP_OVERLAY_DATA) private _data: MapPinData) { + setTimeout(() => this.show_text = true, 300); } } diff --git a/projects/library/src/lib/components/overlays/map-pin/map-pin.styles.scss b/projects/library/src/lib/components/overlays/map-pin/map-pin.styles.scss deleted file mode 100644 index 163c3c7..0000000 --- a/projects/library/src/lib/components/overlays/map-pin/map-pin.styles.scss +++ /dev/null @@ -1,67 +0,0 @@ - -.map-pin { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - height: 1px; - width: 1px; - pointer-events: none; - * { - user-select: none; - } -} - -.pin { - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 2.5em; - height: 3em; - animation: enter 300ms ease-out; -} - -.details { - position: absolute; - bottom: 3.8em; - left: 50%; - transform: translateX(-50%); - background-color: #fff; - padding: .5em .75em; - border-radius: .2em; - box-shadow: 0 1px 3px 0 rgba(#000, .2), 0 1px 1px 0 rgba(#000, .14), 0 2px 1px -1px rgba(#000, .12); - min-width: 5em; - white-space: nowrap; - text-align: center; - animation: enter-delay 600ms ease-out; -} - -@keyframes enter { - 0% { - transform: translate(-50%, -100%); - opacity: 0; - } - - 100% { - transform: translate(-50%, 0%); - opacity: 1; - } -} - -@keyframes enter-delay { - 0% { - transform: translate(-50%, -100%); - opacity: 0; - } - - 50% { - transform: translate(-50%, -100%); - opacity: 0; - } - - 100% { - transform: translate(-50%, 0%); - opacity: 1; - } -} diff --git a/projects/library/src/lib/components/overlays/map-pin/map-pin.template.html b/projects/library/src/lib/components/overlays/map-pin/map-pin.template.html deleted file mode 100644 index 49ea91f..0000000 --- a/projects/library/src/lib/components/overlays/map-pin/map-pin.template.html +++ /dev/null @@ -1,26 +0,0 @@ -<div class="map-pin" widget [@show]> - <div class="pin"> - <svg - version="1.1" - xmlns="http:// www.w3.org/2000/svg" - xmlns:xlink="http:// www.w3.org/1999/xlink" - x="0px" - y="0px" - viewBox="0 0 53 65.7" - style="enable-background:new 0 0 53 65.7;" - xml:space="preserve" - > - <g> - <circle [style.fill]="fore" cx="27.6" cy="21.8" r="13.1" /> - <path - [style.fill]="fore" - [style.stroke]="back" - stroke-width="2.5" - stroke-miterlimit="10" - d="M27.6,4c9.9,0,18,8.1,18,18s-17.1,38.2-18,39.6c-0.9-1.5-18-29.7-18-39.6S17.7,4,27.6,4z M27.6,32.8 c6,0,10.8-4.8,10.8-10.8s-4.8-10.8-10.8-10.8S16.8,16,16.8,22S21.6,32.8,27.6,32.8" - /> - </g> - </svg> - </div> - <div class="details" *ngIf="text">{{ text }}</div> -</div> diff --git a/projects/library/src/lib/components/overlays/map-radius/map-radius.component.html b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.html new file mode 100644 index 0000000..0318e2a --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.html @@ -0,0 +1,4 @@ +<div class="radius" [style.height]="diameter * zoom + 'em'" [style.width]="diameter * zoom + 'em'"> + <div class="background" [style.background-color]="fill"></div> + <div class="text" *ngIf="text">{{ text }}</div> +</div> diff --git a/projects/library/src/lib/components/overlays/map-radius/map-radius.component.scss b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.scss new file mode 100644 index 0000000..647b69e --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.scss @@ -0,0 +1,33 @@ + +.radius { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + min-width: 2vmin; + min-height: 2vmin; + will-change: height, width; +} + +.background { + height: 100%; + width: 100%; + border: 4px dashed rgba(#000, .25); + border-radius: 100%; + opacity: .6; +} + +.text { + position: absolute; + top: -.5em; + left: 50%; + transform: translate(-50%, -100%); + background-color: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px 0 rgba(#000, .2), + 0 1px 1px 0 rgba(#000, .14), + 0 2px 1px -1px rgba(#000, .12); + white-space: nowrap; + font-size: .8em; + padding: .5em .75em; +} diff --git a/projects/library/src/lib/components/overlays/map-radius/map-radius.component.spec.ts b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.spec.ts new file mode 100644 index 0000000..91755ff --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.spec.ts @@ -0,0 +1,61 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { MapRadiusComponent } from './map-radius.component'; +import { BehaviorSubject } from 'rxjs'; +import { MapState, MAP_STATE, MAP_OVERLAY_DATA } from '../../../helpers/map.interfaces'; + +describe('MapRadiusComponent', () => { + let component: MapRadiusComponent; + let fixture: ComponentFixture<MapRadiusComponent>; + let state_obs: any; + + beforeEach(async(() => { + state_obs = new BehaviorSubject<MapState>({ zoom: 1, center: { x: .5, y: .5 } }); + TestBed.configureTestingModule({ + declarations: [MapRadiusComponent], + providers: [ + { provide: MAP_STATE, useValue: state_obs }, + { provide: MAP_OVERLAY_DATA, useValue: {} } + ], + imports: [NoopAnimationsModule] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MapRadiusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show a radius circle', () => { + const compiled: HTMLElement = fixture.debugElement.nativeElement; + const element: HTMLDivElement = compiled.querySelector('.background'); + expect(element).toBeTruthy(); + }); + + it('should show text', () => { + const compiled: HTMLElement = fixture.debugElement.nativeElement; + let element: HTMLDivElement = compiled.querySelector('.text'); + expect(element).toBeFalsy(); + (component as any)._data.text = 'Test'; + fixture.detectChanges(); + element = compiled.querySelector('.text'); + expect(element).toBeTruthy(); + expect(element.textContent).toBe('Test'); + }); + + it('should scale radius with the zoom value', () => { + const compiled: HTMLElement = fixture.debugElement.nativeElement; + const element: HTMLDivElement = compiled.querySelector('.radius'); + expect(element).toBeTruthy(); + expect(element.style.width).toBe('5em'); + component.zoom = 2; + fixture.detectChanges(); + expect(element.style.width).toBe('10em'); + }); +}); diff --git a/projects/library/src/lib/components/overlays/map-radius/map-radius.component.ts b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.ts new file mode 100644 index 0000000..5bd2cc4 --- /dev/null +++ b/projects/library/src/lib/components/overlays/map-radius/map-radius.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit, Inject, OnDestroy } from '@angular/core'; +import { BehaviorSubject, Subscription } from 'rxjs'; + +import { MapState, MAP_OVERLAY_DATA, MAP_STATE } from '../../../helpers/map.interfaces'; + +export interface MapRadiusData { + /** Text to render above the pin */ + readonly text: string; + /** Diameter of the radius circle in em at 100% zoom */ + readonly diameter: number; + /** Fill colour of the map radius circle */ + readonly fill: string; +} + +@Component({ + selector: 'a-map-radius', + templateUrl: './map-radius.component.html', + styleUrls: ['./map-radius.component.scss'] +}) +export class MapRadiusComponent implements OnInit, OnDestroy { + /** Current zoom level of the map */ + public zoom: number = 1; + /** Subscription to the state of the map */ + private _sub: Subscription; + + /** Diameter of the radius circle */ + public get diameter(): number { + return (this._data ? this._data.diameter : 0) || 5; + } + + public get text(): string { + return this._data ? this._data.text : ''; + } + + public get fill(): string { + return (this._data ? this._data.fill : '') || '#f44336'; + } + + + constructor( + @Inject(MAP_OVERLAY_DATA) private _data: MapRadiusData, + @Inject(MAP_STATE) private _state: BehaviorSubject<MapState> + ) {} + + public ngOnInit() { + this._sub = this._state.subscribe((state) => { + this.zoom = state.zoom; + }); + } + + public ngOnDestroy(): void { + if (this._sub) { + this._sub.unsubscribe(); + this._sub = null; + } + } +} diff --git a/projects/library/src/lib/components/overlays/map-range/map-range.component.ts b/projects/library/src/lib/components/overlays/map-range/map-range.component.ts deleted file mode 100644 index d7cf12e..0000000 --- a/projects/library/src/lib/components/overlays/map-range/map-range.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Component } from '@angular/core'; -import { animate, style, transition, trigger } from '@angular/animations'; - -import { AMapFeature } from '../../map-feature/map-feature.class'; -import { BaseWidgetDirective } from '../../../base.directive'; - -@Component({ - selector: 'map-range', - templateUrl: './map-range.template.html', - styleUrls: ['./map-range.styles.scss'], - animations: [ - trigger('show', [ - transition(':enter', [style({ opacity: 0 }), animate(300, style({ opacity: 1 }))]), - transition(':leave', [style({ opacity: 1 }), animate(300, style({ opacity: 0 }))]) - ]) - ] -}) -export class MapRangeComponent extends BaseWidgetDirective { - /** Default diameter of the range circle */ - public diameter = 10; - /** Contextual data associated with the component */ - public context: AMapFeature; - /** Reference size for scaling the cicle */ - public base_size: number; - /** Background colour of the range circle */ - public background: string; - /** Border colour of the range circle */ - public border: string; - /** Display text for the range cirlce */ - public text: string; - - private previous_size = 0; - - constructor(context: AMapFeature) { - super(); - this.context = context || ({} as any); - this.text = this.context.data.text || ''; - this.background = this.context.data.background || this.context.data.bg_alpha || 'rgba(3, 169, 244, .54)'; - this.border = this.context.data.border || this.context.data.bg || '#03A9F4'; - this.base_size = (this.context.zoom || 1) * 5; - this.context.zoomChanges((zoom: number) => this.updateZoom(zoom)); - } - - private updateZoom(zoom: number): void { - if (this.subs.timers.zoom) { - return this.timeout('retry_zoom', () => this.updateZoom(zoom), 10); - } - if (this.previous_size !== zoom) { - this.timeout( - 'zoom', - () => { - this.base_size = Math.round((zoom || 1) * 5); - this.previous_size = this.base_size; - this.subs.timers.zoom = null; - }, - 10 - ); - } - } -} diff --git a/projects/library/src/lib/components/overlays/map-range/map-range.styles.scss b/projects/library/src/lib/components/overlays/map-range/map-range.styles.scss deleted file mode 100644 index 0cb5056..0000000 --- a/projects/library/src/lib/components/overlays/map-range/map-range.styles.scss +++ /dev/null @@ -1,68 +0,0 @@ - -.map-range { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - height: 1px; - width: 1px; - * { - user-select: none; - } -} - -.range { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-radius: 100%; - border: 3px dashed #03A9F4; - background-color: rgba(3, 169, 244, .54); - transition: height 100ms, width 100ms; - will-change: height, width; - animation: enter 300ms ease-out; -} - -.details { - position: absolute; - bottom: 3em; - left: 50%; - transform: translateX(-50%); - background-color: #fff; - padding: .5em .75em; - border-radius: .2em; - box-shadow: 0 1px 3px 0 rgba(#000,.2), 0 1px 1px 0 rgba(#000,.14), 0 2px 1px -1px rgba(#000,.12); - min-width: 5em; - white-space: nowrap; - transition: bottom 200ms; - text-align: center; - animation: enter-delay 600ms ease-out; -} - -@keyframes enter { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@keyframes enter-delay { - 0% { - transform: translate(-50%, -100%); - opacity: 0; - } - - 50% { - transform: translate(-50%, -100%); - opacity: 0; - } - - 100% { - transform: translate(-50%, 0%); - opacity: 1; - } -} diff --git a/projects/library/src/lib/components/overlays/map-range/map-range.template.html b/projects/library/src/lib/components/overlays/map-range/map-range.template.html deleted file mode 100644 index eb48da5..0000000 --- a/projects/library/src/lib/components/overlays/map-range/map-range.template.html +++ /dev/null @@ -1,18 +0,0 @@ -<div - class="map-range" - widget - [@show] - [style.height]="base_size + 'px'" - [style.width]="base_size + 'px'" -> - <div - class="range" - [style.height]="diameter * 100 + '%'" - [style.width]="diameter * 100 + '%'" - [style.background-color]="background" - [style.border-color]="border" - ></div> - <div class="details" [style.bottom]="'calc(' + ((diameter * 102) / 2 ) + '% + .5em)' | safe:'style'"> - {{ text }} - </div> -</div> diff --git a/projects/library/src/lib/directives/map-center.directive.spec.ts b/projects/library/src/lib/directives/map-center.directive.spec.ts new file mode 100644 index 0000000..c5b0b97 --- /dev/null +++ b/projects/library/src/lib/directives/map-center.directive.spec.ts @@ -0,0 +1,12 @@ +// import { MapCenterDirective } from './map-center.directive'; + +// describe('MapCenterDirective', () => { +// let element: any; +// let renderer: any; + +// it('should create an instance', () => { +// renderer = jasmine.createSpyObj('Renderer2', ['listen']); +// const directive = new MapCenterDirective(element, renderer); +// expect(directive).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/directives/map-center.directive.ts b/projects/library/src/lib/directives/map-center.directive.ts new file mode 100644 index 0000000..1ec0c4a --- /dev/null +++ b/projects/library/src/lib/directives/map-center.directive.ts @@ -0,0 +1,133 @@ +import { Directive, Input, Output, HostListener, ElementRef, EventEmitter, Renderer2, OnDestroy } from '@angular/core'; + +import { Point } from '../helpers/type.helpers'; +import { eventToPoint, diffPoints, insideRect } from '../helpers/map.helpers'; +import { RenderableMap } from '../classes/renderable-map'; + +@Directive({ + selector: '[map-input][center]' +}) +export class MapCenterDirective implements OnDestroy { + /** Map to be rendered */ + @Input() public map: RenderableMap; + /** Zoom level of the map as a whole number. 1 = 100% zoom */ + @Input() public zoom: number; + /** + * Position of the center point of the component on the map displayed + * + * For example: + * + * { x: 0, y: 0 } + * Places the map top left corner in the middle of the component + * + * { x: 0.5, y: 0.5 } + * Places the center of the map in the middle of the component + * + * { x: 1, y: 1 } + * Places the bottom right corner of the map in the middle of the component + */ + @Input() public center: Point; + /** Element containing the map */ + @Input() public element: ElementRef<HTMLDivElement>; + /** Emitter for changes to the center value */ + @Output() public centerChange = new EventEmitter<Point>(); + /** Bound box of the host element */ + private _box: ClientRect; + /** Bound box of the host element */ + private _parent_box: ClientRect; + /** Starting position of the move events */ + private _move_start: Point; + /** Listener for move events */ + private _move_listener: () => void; + /** Listener for move events */ + private _end_listener: () => void; + + @HostListener('mousedown', ['$event']) + public startMoveMouse(event: MouseEvent) { + this.startMove(event); + } + + @HostListener('touchstart', ['$event']) + public startMoveTouch(event: any) { + this.startMove(event); + } + + constructor(private _element: ElementRef<HTMLDivElement>, private _renderer: Renderer2) { + this.updateHostElementBox(); + } + + public ngOnDestroy(): void { + this.cleanListeners(); + } + + /** Clean up existing listeners */ + public cleanListeners() { + if (this._move_listener) { + this._move_listener(); + this._move_listener = null; + } + if (this._end_listener) { + this._end_listener(); + this._end_listener = null; + } + } + + private startMove(event: MouseEvent | TouchEvent) { + event.preventDefault(); + this.updateHostElementBox(); + this._move_start = eventToPoint(event); + this.cleanListeners(); + if ((event as any).touches && (event as any).touches.length !== 1) { + return; + } + if (event instanceof MouseEvent) { + this._move_listener = this._renderer.listen('window', 'mousemove', (e: MouseEvent) => + this.move(e) + ); + this._end_listener = this._renderer.listen('window', 'mouseup', _ => + this.cleanListeners() + ); + } else { + this._move_listener = this._renderer.listen('window', 'touchmove', (e: TouchEvent) => + this.move(e) + ); + this._end_listener = this._renderer.listen('window', 'touchend', _ => + this.cleanListeners() + ); + } + } + + /** + * Update the position of the map based of the pointer position + * @param event Last pointer move event + */ + private move(event: MouseEvent | TouchEvent) { + if (!this._box) { return; } + const position = eventToPoint(event); + if (!insideRect(position, this._parent_box)) { return; } + event.preventDefault(); + const diff = diffPoints(position, this._move_start); + const width = this._box.width; + const height = this._box.height; + const change = { + x: diff.x / (width || 1), + y: diff.y / (height || 1) + }; + this.center = { + x: Math.min(1, Math.max(0, this.center.x - change.x)), + y: Math.min(1, Math.max(0, this.center.y - change.y)) + }; + this.centerChange.emit(this.center); + this._move_start = position; + } + + /** Update bounding boxes of parent and map elements */ + private updateHostElementBox() { + if (this.element && this.element.nativeElement) { + this._box = this.element.nativeElement.getBoundingClientRect(); + } + if (this._element && this._element.nativeElement) { + this._parent_box = this._element.nativeElement.getBoundingClientRect(); + } + } +} diff --git a/projects/library/src/lib/directives/map-input.directive.ts b/projects/library/src/lib/directives/map-input.directive.ts deleted file mode 100644 index ef83f66..0000000 --- a/projects/library/src/lib/directives/map-input.directive.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { Directive, Input, Output, EventEmitter, HostListener, Renderer2, SimpleChanges, ElementRef, OnChanges } from "@angular/core"; - -import { BaseWidgetDirective } from '../base.directive'; -import { MapService } from '../services/map.service'; -import { MapUtilities } from '../utlities/map.utilities'; -import { IMapPoint, IMapFeature } from '../components/map.interfaces'; - -export interface IMapListener { - id: string; - selector: string; - event: string; - callback: Function; -} - -interface POILocation { - scale: number; - center: IMapPoint; -} - -@Directive({ - selector: '[aca-map-input]', -}) -export class MapInputDirective extends BaseWidgetDirective implements OnChanges { - @Input() public scale: number; - @Input() public center: IMapPoint; - @Input() public listeners: IMapListener[]; - @Input() public focus: IMapFeature; - @Input() public map: SVGElement; - @Input() public src: string; - @Input() public lock: boolean; - @Output() public scaleChange = new EventEmitter(); - @Output() public centerChange = new EventEmitter(); - @Output() public event = new EventEmitter(); - - private model: { [name: string]: any } = {}; - - private _active_listeners: (() => void)[] = []; - - private map_box: ClientRect; - private lookup: { [src: string]: { [id: string]: POILocation } } = {}; - - constructor(private el: ElementRef<HTMLElement>, private service: MapService, private renderer: Renderer2) { - super(); - } - - public ngOnChanges(changes: SimpleChanges) { - super.ngOnChanges(changes); - if (changes.src) { - if (!this.lookup[this.src]) { - this.lookup[this.src] = {}; - } - } - if (changes.map && this.map) { - this.update(); - } - if (changes.listeners) { - this.updateListeners(); - } - if (changes.focus) { - this.focusItem(); - } - } - - /** - * Start of move event - * @param e - */ - @HostListener('touchstart', ['$event']) public touchMoveStart(e) { - if (this.lock) { return; } - e.center = e.center || { x: e.touches[0].clientX, y: e.touches[0].clientY }; - this.moveStart(e); - this.model.listen_move = this.renderer.listen('window', 'touchmove', (e) => this.move(e)); - this.model.listen_move_end = this.renderer.listen('window', 'touchend', (e) => this.moveEnd()); - } - - /** - * Start of move event - * @param e - */ - @HostListener('mousedown', ['$event']) public mouseMoveStart(e) { - if (this.lock) { return; } - e.center = e.center || { x: e.clientX, y: e.clientY }; - this.moveStart(e); - this.model.listen_move = this.renderer.listen('window', 'mousemove', (e) => this.move(e)); - this.model.listen_move_end = this.renderer.listen('window', 'mouseup', (e) => this.moveEnd()); - } - - /** - * End move events on contextmenu action - * @param e - */ - @HostListener('contextmenu', ['$event']) public onContext(e) { - this.timeout('contextmenu', () => this.moveEnd()); - } - - public moveStart(e) { - if (this.model.listen_move) { - this.model.listen_move(); - this.model.listen_move = null; - } - if (this.model.listen_move_end) { - this.model.listen_move_end(); - this.model.listen_move_end = null; - } - this.model.center_start = this.center; - this.model.target = e.target; - this.model.dx = e.center.x; - this.model.dy = e.center.y; - } - - public moveEnd() { - if (this.model.listen_move) { - this.model.listen_move(); - this.model.listen_move = null; - } - if (this.model.listen_move_end) { - this.model.listen_move_end(); - this.model.listen_move_end = null; - } - } - - - public get isIE() { - return navigator.appName == 'Microsoft Internet Explorer' || !!(navigator.userAgent.match(/Trident/) || navigator.userAgent.match(/rv:11/)) || !!navigator.userAgent.match(/MSIE/g); - } - - /** - * Move event - * @param e - */ - public move(e) { - if (this.lock) { return; } - e.center = e.center || { x: e.clientX || e.touches[0].clientX, y: e.clientY || e.touches[0].clientY }; - const dx = e.center.x - this.model.dx; - const dy = e.center.y - this.model.dy; - const center = this.model.center_start || { x: .5, y: .5 } - const delta_x = +((dx / this.map_box.width) / this.scale).toFixed(4); - const delta_y = +((dy / this.map_box.height) / this.scale).toFixed(4); - this.model.center = { x: center.x - delta_x, y: center.y - delta_y }; - this.changePosition(); - } - - @HostListener('wheel', ['$event']) public wheelZoom(e) { - if (this.lock) { return; } - e.preventDefault(); - this.model.scale = this.scale || 1; - const rate = (this.isIE ? 1.15 : 1.05); - this.model.scale *= (e.deltaY < 0 ? rate : (e.deltaY > 0 ? (1 / rate) : 1)); - this.check(); - this.scale = +(this.model.scale * 10000).toFixed(0) / 10000; - this.scaleChange.emit(this.scale); - } - - @HostListener('pinchstart', ['$event']) public pinchStart(e) { - if (this.lock) { return; } - this.model.dz = e.scale; - const box = this.map_box; - const dist = Math.sqrt(box.width * box.width + box.height * box.height); - const width = e.pointers[0].clientX - e.pointers[1].clientX; - const height = e.pointers[0].clientY - e.pointers[1].clientY; - const pinch_dist = Math.sqrt(width * width + height * height); - this.model.in = 10 * (pinch_dist / dist); - } - - @HostListener('pinchmove', ['$event']) public pinchZoom(e) { - if (this.lock) { return; } - e.preventDefault(); - const dz = e.scale - this.model.dz; - this.model.scale = this.scale || 1; - this.model.scale += this.model.in > 1 ? dz * this.model.in : dz; - this.check(); - this.scale = +(this.model.scale * 10000).toFixed(0) / 10000; - this.scaleChange.emit(this.scale); - this.model.dz = e.scale; - } - - /** - * Focus on the given point of interest - */ - public focusItem() { - if (this.focus && (this.focus.id || this.focus.coordinates)) { - if (!this.map) { - return this.timeout('focus', () => this.focusItem()); - } - this.map_box = this.el.nativeElement.getBoundingClientRect(); - const selector = this.focus.id ? `#${MapUtilities.cleanCssSelector(this.focus.id)}` : '' - const el = this.focus.id ? this.map.querySelector(selector) : null; - if (el) { // Focus on element - if (this.lookup[this.src][selector]) { - this.model.center = this.lookup[this.src][selector].center; - this.model.scale = this.focus.zoom ? this.focus.zoom / 100 : this.lookup[this.src][selector].scale; - } else { - const box = el.getBoundingClientRect(); - const map_box = this.map.getBoundingClientRect(); - this.model.center = { - x: ((box.left + box.width / 2) - map_box.left) / map_box.width, - y: ((box.top + box.height / 2) - map_box.top) / map_box.height - }; - this.lookup[this.src][selector] = { - scale: Math.min(2, MapUtilities.getFillScale(map_box, box) * .4), - center: { ...this.model.center } - }; - this.model.scale = this.focus.zoom ? this.focus.zoom / 100 : this.lookup[this.src][selector].scale; - } - this.changePosition(true) - } else if (this.focus.coordinates) { // Focus on coordinates - const pnt = this.focus.coordinates; - const ratio = this.map_box.height / this.map_box.width; - this.model.center = { x: pnt.x / 10000, y: pnt.y / (10000 * ratio) }; - this.model.scale = this.focus.zoom ? this.focus.zoom / 100 : 1; - this.changePosition(true); - } - } - } - - /** - * Update bounding box of map - */ - public update() { - if (this.map) { - this.map_box = this.el.nativeElement.getBoundingClientRect(); - if (this.map_box.height === 0 || this.map_box.width === 0) { - return this.timeout('update_fail', () => this.update()); - } - this.model.scale = 1; - this.model.center = { x: .5, y: .5 }; - if (this.focus) { - this.focusItem(); - } else { - this.changePosition(true); - } - - } - } - - /** - * Make sure center and scale are within bounds - */ - public check() { - if (!this.model.center) { this.model.center = { x: .5, y: .5 }; } - // Make sure 0 <= x <= 1 - if (this.model.center.x < 0) { this.model.center.x = 0; } - else if (this.model.center.x > 1) { this.model.center.x = 1; } - // Make sure 0 <= y <= 1 - if (this.model.center.y < 0) { this.model.center.y = 0; } - else if (this.model.center.y > 1) { this.model.center.y = 1; } - // Make sure 100 <= zoom <= 1000 - if (this.model.scale < 1) { this.model.scale = 1; } - else if (this.model.scale > 10) { this.model.scale = 10; } - } - - /** - * Update listeners for map events - */ - private updateListeners() { - if (!this.map) { - return this.timeout('listeners', () => this.updateListeners()); - } - this.clearListeners(); - if (this.listeners) { - for (const item of this.listeners) { - const selector = item.id ? MapUtilities.cleanCssSelector(`#${item.id}`) : `${MapUtilities.cleanCssSelector(item.selector)}`; - const el = this.map.querySelector(selector); - if (el) { - this.renderer.setStyle(el, 'pointer-events', 'auto'); - this.renderer.setStyle(el, 'display', 'inline-block'); - const unsub = this.renderer.listen(el, item.event || 'click', () => { - if (item.callback) { item.callback(); } - else { this.event.emit({ id: selector, type: 'listener_event' }); } - }) - this._active_listeners.push(() => { - this.renderer.setStyle(el, 'pointer-events', ''); - this.renderer.setStyle(el, 'display', ''); - unsub(); - }); - } else { - this.service.log('Warn', `Unable to find element with selector '${item.id ? `#${item.id}` : `${item.selector}`}'`, item.id ? `#${item.id}` : `${item.selector}`) - } - } - this.model.listeners = this.listeners; - } - } - - /** - * Remove existing listeners from map - */ - private clearListeners() { - if (this._active_listeners) { - for (const listener of this._active_listeners) { - listener(); - } - delete this._active_listeners; - this._active_listeners = []; - } - } - - /** - * Post changes position of the map - * @param scale Also post changes to scale - */ - private changePosition(scale: boolean = false) { - this.timeout('change', () => { - this.check(); - this.center = this.model.center; - this.centerChange.emit(this.center); - if (scale) { - this.scale = +(this.model.scale * 10000).toFixed(0) / 10000; - this.scaleChange.emit(this.scale); - } - }, 0); - } -} diff --git a/projects/library/src/lib/directives/map-styler.directive.ts b/projects/library/src/lib/directives/map-styler.directive.ts deleted file mode 100644 index c8c008f..0000000 --- a/projects/library/src/lib/directives/map-styler.directive.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Directive, Input, Output, EventEmitter, OnChanges, Renderer2, SimpleChanges } from '@angular/core'; - -import { BaseWidgetDirective } from '../base.directive'; -import { MapUtilities } from '../utlities/map.utilities'; -import { IStyleMappings } from '../components/map.interfaces'; - -export interface IMapListener { - id: string; - event: string; -} - -@Directive({ - selector: '[aca-map-styler]', -}) -export class MapStylerDirective extends BaseWidgetDirective implements OnChanges { - @Input() public id: string; - @Input('cssStyles') public styles: IStyleMappings; - @Input() public map: Element; - @Output() public css = new EventEmitter<string>(); - - private model: { [name: string]: any } = {}; - - constructor(private renderer: Renderer2) { super(); } - - public ngOnChanges(changes: SimpleChanges) { - if (changes.styles) { - this.update(); - } - } - - public update() { - this.clear(); - if (this.styles) { - let css = ''; - for (const selector in this.styles) { - if (this.styles.hasOwnProperty(selector)) { - let style = `.map [map-${this.id}] ${MapUtilities.cleanCssSelector(selector)} { `; - for (const property in this.styles[selector]) { - if (this.styles[selector][property]) { - style += `${property}: ${this.styles[selector][property]}; `; - } - } - style += '} '; - css += style; - } - } - this.model.css = css; - this.model.style_el = document.createElement('style'); - this.model.style_el.innerHTML = css; - this.renderer.appendChild(document.head, this.model.style_el); - const replaced = this.model.css.replace(new RegExp(`\\.map \\[map-${this.id}\\]`, 'g'), ''); - this.css.emit(replaced); - } - } - - public clear() { - if (this.model.css && this.model.style_el) { - this.renderer.removeChild(document.head, this.model.style_el); - this.model.css = ''; - } - } -} diff --git a/projects/library/src/lib/directives/map-zoom.directive.spec.ts b/projects/library/src/lib/directives/map-zoom.directive.spec.ts new file mode 100644 index 0000000..957955f --- /dev/null +++ b/projects/library/src/lib/directives/map-zoom.directive.spec.ts @@ -0,0 +1,14 @@ +// import { MapZoomDirective } from './map-zoom.directive'; + +// describe('MapZoomDirective', () => { +// let element: any; +// let move_directive: any; +// let renderer: any; + +// it('should create an instance', () => { +// renderer = jasmine.createSpyObj('Renderer2', ['listen']); +// move_directive = jasmine.createSpyObj('MapCenterDirective', ['cleanListeners']); +// const directive = new MapZoomDirective(element, renderer, move_directive); +// expect(directive).toBeTruthy(); +// }); +// }); diff --git a/projects/library/src/lib/directives/map-zoom.directive.ts b/projects/library/src/lib/directives/map-zoom.directive.ts new file mode 100644 index 0000000..f0a12dd --- /dev/null +++ b/projects/library/src/lib/directives/map-zoom.directive.ts @@ -0,0 +1,126 @@ +import { Directive, Input, EventEmitter, Output, ElementRef, HostListener, SimpleChanges, Renderer2 } from '@angular/core'; +import { Point } from '../helpers/type.helpers'; +import { eventToPoint } from '../helpers/map.helpers'; +import { MapCenterDirective } from './map-center.directive'; + +@Directive({ + selector: '[map-input][zoom]' +}) +export class MapZoomDirective { + /** Zoom level of the map as a whole number. 1 = 100% zoom */ + @Input() public zoom: number; + /** Emitter for changes to the zoom value */ + @Output() public zoomChange = new EventEmitter<number>(); + /** Zoom level of the map as a whole number. 1 = 100% zoom */ + @Input() public center: Point; + + @Input() public element: ElementRef<HTMLDivElement>; + /** Emitter for changes to the zoom value */ + @Output() public centerChange = new EventEmitter<Point>(); + + /** Bounding box of the map element */ + private _box: ClientRect; + /** Bound box of the host element */ + private _parent_box: ClientRect; + + private _pinch_listener: () => void; + + private _pinch_end_listener: () => void; + + private _pinch_delta: number; + + constructor(private _element: ElementRef<HTMLDivElement>, private _renderer: Renderer2, private _move_directive: MapCenterDirective) { + this.updateHostElementBox(); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes.element) { + this.updateHostElementBox(); + } + } + + public ngOnDestroy(): void { + this.clearListeners(); + } + + @HostListener('wheel', ['$event']) + public wheelScroll(event: WheelEvent) { + event.preventDefault(); + const delta = -event.deltaY / 100; + const new_zoom = Math.min(10, Math.max(1, this.zoom + delta / 5)); + this.updateCenter(this.zoom, new_zoom, eventToPoint(event)); + this.zoom = Math.floor(new_zoom * 1000) / 1000; + this.zoomChange.emit(this.zoom); + } + + @HostListener('pinchstart', ['$event']) + public pinchStart(event: any) { + console.log('Event:', event); + this._pinch_delta = event.scale || 1; + event.preventDefault(); + if (this._move_directive) { this._move_directive.cleanListeners(); } + this._pinch_listener = this._renderer.listen('window', 'pinchmove', (e) => this.onPinch(e)); + this._pinch_end_listener = this._renderer.listen('window', 'pinchend', (e) => this.clearListeners()); + } + + @HostListener('dblclick', ['$event']) + public tapZoom(event: MouseEvent) { + const new_zoom = Math.min(10, Math.max(1, this.zoom * 1.5)); + this.updateCenter(this.zoom, new_zoom, eventToPoint(event)); + this.zoom = new_zoom; + this.zoomChange.emit(this.zoom); + } + + public onPinch(event: any) { + event.preventDefault(); + const delta = (event.scale - this._pinch_delta) / 4; + const delta_zoom = delta < 0 ? delta * 4 : delta; + console.log('Delta:', delta); + const new_zoom = Math.min(10, Math.max(1, this.zoom * (1 + delta_zoom))); + this.zoom = new_zoom; + this.zoomChange.emit(this.zoom); + this._pinch_delta = event.scale || 1; + } + + + private updateCenter(old_zoom: number, new_zoom: number, position: Point) { + // if (!this.element || !this.element.nativeElement) { return; } + // this.updateHostElementBox(); + // const zoom_diff = new_zoom - old_zoom; + // const point: Point = { + // x: (position.x - this._box.left) / this._box.width, + // y: (position.y - this._box.top) / this._box.height, + // }; + // const old_center = this.center; + // this.center = { + // x: Math.max(0, Math.min(1, this.center.x - (this.center.x - point.x) * (zoom_diff))), + // y: Math.max(0, Math.min(1, this.center.y - (this.center.y - point.y) * (zoom_diff))) + // }; + // console.log('Update center:', (zoom_diff * 100).toFixed(2), { + // x: ((point.x - old_center.x) * 100).toFixed(2), + // y: ((point.y - old_center.y) * 100).toFixed(2) + // }, { x: (this.center.x * 100).toFixed(2), y: (this.center.y * 100).toFixed(2) }); + // this.centerChange.emit(this.center); + } + + /** Update bounding boxes of parent and map elements */ + private updateHostElementBox() { + if (this.element && this.element.nativeElement) { + this._box = this.element.nativeElement.getBoundingClientRect(); + } + if (this._element && this._element.nativeElement) { + this._parent_box = this._element.nativeElement.getBoundingClientRect(); + } + } + + private clearListeners() { + if (this._pinch_listener) { + this._pinch_listener(); + this._pinch_listener = null; + } + if (this._pinch_end_listener) { + this._pinch_end_listener(); + this._pinch_end_listener = null; + } + } +} diff --git a/projects/library/src/lib/helpers/map.helpers.ts b/projects/library/src/lib/helpers/map.helpers.ts new file mode 100644 index 0000000..981d31f --- /dev/null +++ b/projects/library/src/lib/helpers/map.helpers.ts @@ -0,0 +1,79 @@ +import { Point } from './type.helpers'; + +/** Encode string into a base 64 string */ +export function base64Encode(str) { + // first we use encodeURIComponent to get percent-encoded UTF-8, + // then we convert the percent encodings into raw bytes which + // can be fed into btoa. + const solid = (match, p1) => String.fromCharCode(('0x' + p1) as any); + return btoa((encodeURIComponent(str) as any).replace(/%([0-9A-F]{2})/g, solid)); +} + +export function getFillScale(source, dest) { + const ratio_w = source.width / dest.width; + const ratio_h = source.height / dest.height; + return Math.min(ratio_w, ratio_h); +} + +export function cleanCssSelector(name) { + let selector = name.replace(/[!"#$%&'()*+,.\/;<=>?@[\\\]^`{|}~]/g, '\\$&'); + const parts = selector.split(' '); + for (const p of parts) { + parts.splice(parts.indexOf(p), 1, [p.replace(/^\\/g, '')]); + } + selector = parts.join(' '); + return selector; +} + +export function getPosition(container: ClientRect, element: ClientRect): Point { + const position = { + x: element.left - container.left + element.width / 2, + y: element.top - container.top + element.height / 2 + }; + return { + x: +(position.x / container.width).toFixed(3), + y: +(position.y / container.height).toFixed(3) + }; +} + +/** + * Grab point details from mouse or touch event + * @param event Event to grab details from + */ +export function eventToPoint(event: MouseEvent | TouchEvent): Point { + if (!event) { + return { x: -1, y: -1 }; + } + if (event instanceof MouseEvent) { + return { x: event.clientX, y: event.clientY }; + } else { + return event.touches && event.touches.length > 0 + ? { x: event.touches[0].clientX, y: event.touches[0].clientY } + : { x: -1, y: -1 }; + } +} + +export function diffPoints(first: Point, second: Point): Point { + return { + x: first.x - second.x, + y: first.y - second.y + }; +} + +export function staggerChange(value: any, callback: (v: any) => any) { + return new Promise<void>(resolve => { + requestAnimationFrame(() => { + const progress = callback(value || 0); + if (progress) { + staggerChange(progress, callback).then(() => resolve()); + } else { + resolve(); + } + }); + }); +} + +/** Whether point is inside the rectangle */ +export function insideRect(point: Point, rect: ClientRect): boolean { + return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom; +} diff --git a/projects/library/src/lib/helpers/map.interfaces.ts b/projects/library/src/lib/helpers/map.interfaces.ts new file mode 100644 index 0000000..0573427 --- /dev/null +++ b/projects/library/src/lib/helpers/map.interfaces.ts @@ -0,0 +1,46 @@ +import { TemplateRef, Type, InjectionToken } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +import { Point } from './type.helpers'; + +/** Emitter for changes to the state of the map */ +export const MAP_STATE = new InjectionToken<BehaviorSubject<MapState>>('MapState'); +/** Location of the overlay component or template on the map */ +export const MAP_LOCATION = new InjectionToken<Point>('MapLocation'); +/** Data to pass to the overlay component or template */ +export const MAP_OVERLAY_DATA = new InjectionToken<any>('MapOverlayData'); + +export interface MapFeature<T = any> { + /** Map element id attribute to associate the feature with */ + readonly id?: string; + /** Map coordinates */ + readonly coordinates: Point; + /** Contents of the map feature */ + readonly content: Type<any> | TemplateRef<any> | string; + /** Data to pass into the contents */ + readonly data: T; +} + +export interface MapTextFeature extends MapFeature { + readonly content: string; + /** Minimum zoom level to show the text feature */ + readonly show_after_zoom?: number; + /** Map of CSS properties to their values */ + readonly styles?: { [style: string]: string }; +} + +export interface MapState { + /** Current zoom level of the map */ + readonly zoom: number; + /** Current center point of the map */ + readonly center: Point; +} + +export interface MapListener { + /** ID of the element to listen to events on */ + readonly id: string; + /** Name of the event to listen for */ + readonly event: string; + /** Callback when the event occurs */ + readonly callback: (event: Event) => void; +} diff --git a/projects/library/src/lib/helpers/type.helpers.ts b/projects/library/src/lib/helpers/type.helpers.ts new file mode 100644 index 0000000..061af7a --- /dev/null +++ b/projects/library/src/lib/helpers/type.helpers.ts @@ -0,0 +1,26 @@ +import { MapRenderFeature } from '../classes/map-render-feature'; +import { TemplateRef, Type } from '@angular/core'; + +/** Coordinates pair for the map */ +export interface Point { + /** Coordinate on the X axis */ + readonly x: number; + /** Coordinate on the Y axis */ + readonly y: number; +} + +export interface HashMap<T = any> { + [key: string]: T; +} + +export type RenderFeature = TemplateRef<any> | Type<any> | string; + +export interface MapOptions { + /** Fix the position and zoom of the map */ + readonly lock: boolean; +} + +export interface MapEvent<T = any> { + type: 'click' | 'action'; + metadata: T; +} diff --git a/projects/library/src/lib/library.module.ts b/projects/library/src/lib/library.module.ts index af4f17d..71118af 100644 --- a/projects/library/src/lib/library.module.ts +++ b/projects/library/src/lib/library.module.ts @@ -8,41 +8,42 @@ import { version } from './settings'; import * as dayjs_api from 'dayjs'; const dayjs = dayjs_api; -import { AMapComponent } from './components/map/map.component'; -import { MapOverlayOutletComponent } from './components/map-overlay-outlet/map-overlay-outlet.component'; -import { MapRendererComponent } from './components/map-renderer/map-renderer.component'; - -import { MapInputDirective } from './directives/map-input.directive'; -import { MapStylerDirective } from './directives/map-styler.directive'; +import { MapComponent } from './components/map/map.component'; -import { MapPinComponent } from './components/overlays/map-pin/map-pin.component'; -import { MapRangeComponent } from './components/overlays/map-range/map-range.component'; import { BaseWidgetDirective } from './base.directive'; +import { MapOutletComponent } from './components/map-outlet/map-outlet.component'; +import { MapZoomDirective } from './directives/map-zoom.directive'; +import { MapCenterDirective } from './directives/map-center.directive'; +import { MapOverlayOutletComponent } from './components/map-overlay-outlet/map-overlay-outlet.component'; +import { MapTextOutletComponent } from './components/map-text-outlet/map-text-outlet.component'; +import { MapPinComponent } from './components/overlays/map-pin/map-pin.component'; +import { MapRadiusComponent } from './components/overlays/map-radius/map-radius.component'; @NgModule({ declarations: [ BaseWidgetDirective, - AMapComponent, + MapComponent, + MapOutletComponent, + MapZoomDirective, + MapCenterDirective, MapOverlayOutletComponent, - MapRendererComponent, + MapTextOutletComponent, MapPinComponent, - MapRangeComponent, - MapInputDirective, - MapStylerDirective + MapRadiusComponent ], imports: [CommonModule, APipesModule, HttpClientModule], exports: [ - AMapComponent, + MapComponent, MapPinComponent, - MapRangeComponent + MapRadiusComponent ], entryComponents: [ MapPinComponent, - MapRangeComponent + MapRadiusComponent ] }) export class LibraryModule { - public static version = 'local-dev'; + public static version = '0.0.0-development'; private static init = false; readonly build = dayjs(); diff --git a/projects/library/src/lib/services/map.service.ts b/projects/library/src/lib/services/map.service.ts index d6a48af..cb6c797 100644 --- a/projects/library/src/lib/services/map.service.ts +++ b/projects/library/src/lib/services/map.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { log } from '../settings'; +import { RenderableMap } from '../classes/renderable-map'; import * as day_api from 'dayjs'; const dayjs = day_api; @@ -17,9 +18,7 @@ export interface IMapItem<T> { export interface IMapNode { /** Node identifier */ id: string; - [name: string]: any; - /** Children Map nodes */ - children?: IMapNode[]; + data: RenderableMap; } @Injectable({ @@ -27,9 +26,8 @@ export interface IMapNode { }) export class MapService { /** Map SVGs */ - private maps: { [name: string]: IMapItem<string> } = {}; + private maps: { [name: string]: IMapItem<RenderableMap> } = {}; /** Map Nodes */ - private map_trees: { [name: string]: IMapItem<IMapNode> } = {}; public logs: { warning_ids: string[]; warnings: string[]; @@ -41,18 +39,7 @@ export class MapService { }; constructor(private http: HttpClient) { - if (sessionStorage) { - for (let i = 0; i < sessionStorage.length; i++) { - const key = sessionStorage.key(i); - if (key.indexOf('WIDGETS.map.tree.') >= 0) { - const url = key.replace('WIDGETS.map.tree.', '').split('.').join('/') + '.svg'; - const tree = JSON.parse(sessionStorage.getItem(key)); - if (tree && tree.expiry && dayjs().isBefore(dayjs(tree.expiry))) { - this.map_trees[url] = tree; - } - } - } - } + } /** @@ -61,23 +48,15 @@ export class MapService { * @returns Promise of the raw map file, errors with reason */ public loadMap(url: string) { - return new Promise<string>((resolve, reject) => { - if (!url) { return resolve(''); } + return new Promise<RenderableMap>((resolve, reject) => { + if (!url) { return resolve(null); } const now = (new Date()).getTime(); if (this.maps[url] && this.maps[url].expiry > now) { resolve(this.maps[url].data); } else { - let map: string = null; - this.map_trees[url] = null; + let map: RenderableMap = null; this.http.get(url, { responseType: 'text' }).subscribe((data) => { - map = data; - // Prevent non SVG files from being used - if (!map.match(/<\/svg>/g)) { map = ''; } - // Prevent Adobe generic style names from being used - map = map.replace(/cls-/g, `map-${Object.keys(this.maps).length}-`); - map = map.replace(/\.map/g, `svg .map`); - // Remove title tags and content from the map - map = map.replace(/<title>.*<\/title>/gm, ''); + map = new RenderableMap(url, data); }, (err) => reject(err), () => { @@ -86,9 +65,6 @@ export class MapService { expiry: now + MAP_EXPIRY, data: map }; - // if (!this.map_trees[url] || moment().isAfter(moment(this.map_trees[url].expiry))) { - // this.loadMapTree(url); - // } resolve(map); } else { reject('Invalid SVG map'); @@ -98,50 +74,6 @@ export class MapService { }); } - /** - * Loads the map tree of the map with the give URL - * @param url URL of the map - */ - public loadMapTree(url: string) { - return new Promise((resolve, reject) => { - let tree = null; - this.http.get(`${url}.maptree.json`).subscribe( - (data) => tree = data, - (err) => reject(err), - () => { - if (tree) { - this.setMapTree(url, tree, false); - } - } - ) - }); - } - - /** - * Gets the map tree of the map with the given URL - * @param url URL of the map - * @returns Element tree of the map - */ - public getMapTree(url: string) { - return this.map_trees[url] ? this.map_trees[url].data : null; - } - - /** - * Sets the map tree of the map with the given URL - * @param url URL of the map tree's file - * @param tree Element position tree of the given map URL - * @param expire Sets whether the tree data will expire - */ - public setMapTree(url: string, tree: IMapNode, expire: boolean = true) { - let expiry = dayjs().add(1, 'day').valueOf(); - if (!expire) { - expiry = Math.floor(expiry + 365 * 24 * 60 * 60 * 1000); - } - this.map_trees[url] = { expiry, data: tree }; - if (sessionStorage) { - sessionStorage.setItem(`WIDGETS.maps.tree.${url.split('.')[0].split('/').join('.')}`, JSON.stringify(this.map_trees[url])); - } - } /** * Clears all the cached map data diff --git a/projects/library/src/lib/utlities/map.utilities.ts b/projects/library/src/lib/utlities/map.utilities.ts deleted file mode 100644 index 78d09f3..0000000 --- a/projects/library/src/lib/utlities/map.utilities.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { IMapPoint } from '../components/map.interfaces'; - - -/** Utility functions for maps */ -export class MapUtilities { - constructor() { - throw new Error('class is static'); - } - - public static base64Encode(str) { - // first we use encodeURIComponent to get percent-encoded UTF-8, - // then we convert the percent encodings into raw bytes which - // can be fed into btoa. - const solid = (match, p1) => String.fromCharCode(('0x' + p1) as any); - return btoa((encodeURIComponent(str) as any).replace(/%([0-9A-F]{2})/g, solid)); - } - - public static getFillScale(source, dest) { - const ratio_w = source.width / dest.width; - const ratio_h = source.height / dest.height; - return Math.min(ratio_w, ratio_h); - } - - public static cleanCssSelector(name) { - let selector = name.replace(/[!"#$%&'()*+,.\/;<=>?@[\\\]^`{|}~]/g, '\\$&'); - const parts = selector.split(' '); - for (const p of parts) { - parts.splice(parts.indexOf(p), 1, [p.replace(/^\\/g, '')]); - } - selector = parts.join(' '); - return selector; - } - - public static getPosition(box, el: Element, coords: IMapPoint) { - let position = null; - if (el) { - const el_box = el.getBoundingClientRect(); - position = { - x: +(((el_box.left - box.left) / box.width + (el_box.width / 2) / box.width) * 100).toFixed(3), - y: +(((el_box.top - box.top) / box.height + (el_box.height / 2) / box.height) * 100).toFixed(3) - }; - } else if (coords) { - const ratio = box.width / box.height; - position = { - x: +((coords.x / 10000) * 100).toFixed(3), - y: +((coords.y / (10000 * ratio)) * 100).toFixed(3) - }; - } - if (position) { - if (position.x < 0) { - position.x = 0; - } else if (position.x > 100) { - position.x = 100; - } - if (position.y < 0) { - position.y = 0; - } else if (position.y > 100) { - position.y = 100; - } - } - return position; - } -} diff --git a/projects/library/src/public-api.ts b/projects/library/src/public-api.ts index 5addcb4..66fcdea 100644 --- a/projects/library/src/public-api.ts +++ b/projects/library/src/public-api.ts @@ -4,9 +4,11 @@ export * from './lib/library.module'; -export * from './lib/components/map/map.component'; -export * from './lib/components/map.interfaces'; -export * from './lib/components/map-feature/map-feature.class'; +export * from './lib/helpers/map.helpers'; +export * from './lib/helpers/type.helpers'; +export * from './lib/helpers/map.interfaces'; +export * from './lib/components/map/map.component'; export * from './lib/components/overlays/map-pin/map-pin.component'; -export * from './lib/components/overlays/map-range/map-range.component'; \ No newline at end of file +export * from './lib/components/overlays/map-radius/map-radius.component'; +