diff --git a/package-lock.json b/package-lock.json index 1d40de5db..7556626f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@types/bootstrap": "^5.1.4", "@types/bootstrap-notify": "^3.1.34", - "@types/d3": "^5.7.2", "@types/jquery": "^3.3.38", "@types/node": "^14.0.11", "@types/showdown": "^1.9.4", @@ -242,239 +241,6 @@ "@types/jquery": "*" } }, - "node_modules/@types/d3": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.16.4.tgz", - "integrity": "sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w==", - "dependencies": { - "@types/d3-array": "^1", - "@types/d3-axis": "^1", - "@types/d3-brush": "^1", - "@types/d3-chord": "^1", - "@types/d3-collection": "*", - "@types/d3-color": "^1", - "@types/d3-contour": "^1", - "@types/d3-dispatch": "^1", - "@types/d3-drag": "^1", - "@types/d3-dsv": "^1", - "@types/d3-ease": "^1", - "@types/d3-fetch": "^1", - "@types/d3-force": "^1", - "@types/d3-format": "^1", - "@types/d3-geo": "^1", - "@types/d3-hierarchy": "^1", - "@types/d3-interpolate": "^1", - "@types/d3-path": "^1", - "@types/d3-polygon": "^1", - "@types/d3-quadtree": "^1", - "@types/d3-random": "^1", - "@types/d3-scale": "^2", - "@types/d3-scale-chromatic": "^1", - "@types/d3-selection": "^1", - "@types/d3-shape": "^1", - "@types/d3-time": "^1", - "@types/d3-time-format": "^2", - "@types/d3-timer": "^1", - "@types/d3-transition": "^1", - "@types/d3-voronoi": "*", - "@types/d3-zoom": "^1" - } - }, - "node_modules/@types/d3-array": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.9.tgz", - "integrity": "sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==" - }, - "node_modules/@types/d3-axis": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.16.tgz", - "integrity": "sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ==", - "dependencies": { - "@types/d3-selection": "^1" - } - }, - "node_modules/@types/d3-brush": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.5.tgz", - "integrity": "sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw==", - "dependencies": { - "@types/d3-selection": "^1" - } - }, - "node_modules/@types/d3-chord": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.11.tgz", - "integrity": "sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg==" - }, - "node_modules/@types/d3-collection": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.10.tgz", - "integrity": "sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A==" - }, - "node_modules/@types/d3-color": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.2.tgz", - "integrity": "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==" - }, - "node_modules/@types/d3-contour": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-1.3.3.tgz", - "integrity": "sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww==", - "dependencies": { - "@types/d3-array": "^1", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-dispatch": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz", - "integrity": "sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ==" - }, - "node_modules/@types/d3-drag": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.5.tgz", - "integrity": "sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw==", - "dependencies": { - "@types/d3-selection": "^1" - } - }, - "node_modules/@types/d3-dsv": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.1.tgz", - "integrity": "sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA==" - }, - "node_modules/@types/d3-ease": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-1.0.11.tgz", - "integrity": "sha512-wUigPL0kleGZ9u3RhzBP07lxxkMcUjL5IODP42mN/05UNL+JJCDnpEPpFbJiPvLcTeRKGIRpBBJyP/1BNwYsVA==" - }, - "node_modules/@types/d3-fetch": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-1.2.2.tgz", - "integrity": "sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw==", - "dependencies": { - "@types/d3-dsv": "^1" - } - }, - "node_modules/@types/d3-force": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz", - "integrity": "sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w==" - }, - "node_modules/@types/d3-format": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz", - "integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==" - }, - "node_modules/@types/d3-geo": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.3.tgz", - "integrity": "sha512-yZbPb7/5DyL/pXkeOmZ7L5ySpuGr4H48t1cuALjnJy5sXQqmSSAYBiwa6Ya/XpWKX2rJqGDDubmh3nOaopOpeA==", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", - "integrity": "sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg==" - }, - "node_modules/@types/d3-interpolate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz", - "integrity": "sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==", - "dependencies": { - "@types/d3-color": "^1" - } - }, - "node_modules/@types/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==" - }, - "node_modules/@types/d3-polygon": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-1.0.8.tgz", - "integrity": "sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw==" - }, - "node_modules/@types/d3-quadtree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz", - "integrity": "sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA==" - }, - "node_modules/@types/d3-random": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-1.1.3.tgz", - "integrity": "sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ==" - }, - "node_modules/@types/d3-scale": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.2.6.tgz", - "integrity": "sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw==", - "dependencies": { - "@types/d3-time": "^1" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.1.tgz", - "integrity": "sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg==" - }, - "node_modules/@types/d3-selection": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.3.tgz", - "integrity": "sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA==" - }, - "node_modules/@types/d3-shape": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz", - "integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==", - "dependencies": { - "@types/d3-path": "^1" - } - }, - "node_modules/@types/d3-time": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz", - "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==" - }, - "node_modules/@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" - }, - "node_modules/@types/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg==" - }, - "node_modules/@types/d3-transition": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.3.2.tgz", - "integrity": "sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg==", - "dependencies": { - "@types/d3-selection": "^1" - } - }, - "node_modules/@types/d3-voronoi": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz", - "integrity": "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==" - }, - "node_modules/@types/d3-zoom": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.8.3.tgz", - "integrity": "sha512-3kHkL6sPiDdbfGhzlp5gIHyu3kULhtnHTTAl3UBZVtWB1PzcLL8vdmz5mTx7plLiUqOA2Y+yT2GKjt/TdA2p7Q==", - "dependencies": { - "@types/d3-interpolate": "^1", - "@types/d3-selection": "^1" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.10", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" - }, "node_modules/@types/jquery": { "version": "3.5.14", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", @@ -2265,239 +2031,6 @@ "@types/jquery": "*" } }, - "@types/d3": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.16.4.tgz", - "integrity": "sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w==", - "requires": { - "@types/d3-array": "^1", - "@types/d3-axis": "^1", - "@types/d3-brush": "^1", - "@types/d3-chord": "^1", - "@types/d3-collection": "*", - "@types/d3-color": "^1", - "@types/d3-contour": "^1", - "@types/d3-dispatch": "^1", - "@types/d3-drag": "^1", - "@types/d3-dsv": "^1", - "@types/d3-ease": "^1", - "@types/d3-fetch": "^1", - "@types/d3-force": "^1", - "@types/d3-format": "^1", - "@types/d3-geo": "^1", - "@types/d3-hierarchy": "^1", - "@types/d3-interpolate": "^1", - "@types/d3-path": "^1", - "@types/d3-polygon": "^1", - "@types/d3-quadtree": "^1", - "@types/d3-random": "^1", - "@types/d3-scale": "^2", - "@types/d3-scale-chromatic": "^1", - "@types/d3-selection": "^1", - "@types/d3-shape": "^1", - "@types/d3-time": "^1", - "@types/d3-time-format": "^2", - "@types/d3-timer": "^1", - "@types/d3-transition": "^1", - "@types/d3-voronoi": "*", - "@types/d3-zoom": "^1" - } - }, - "@types/d3-array": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.9.tgz", - "integrity": "sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==" - }, - "@types/d3-axis": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.16.tgz", - "integrity": "sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ==", - "requires": { - "@types/d3-selection": "^1" - } - }, - "@types/d3-brush": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.5.tgz", - "integrity": "sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw==", - "requires": { - "@types/d3-selection": "^1" - } - }, - "@types/d3-chord": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.11.tgz", - "integrity": "sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg==" - }, - "@types/d3-collection": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.10.tgz", - "integrity": "sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A==" - }, - "@types/d3-color": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.2.tgz", - "integrity": "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==" - }, - "@types/d3-contour": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-1.3.3.tgz", - "integrity": "sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww==", - "requires": { - "@types/d3-array": "^1", - "@types/geojson": "*" - } - }, - "@types/d3-dispatch": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz", - "integrity": "sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ==" - }, - "@types/d3-drag": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.5.tgz", - "integrity": "sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw==", - "requires": { - "@types/d3-selection": "^1" - } - }, - "@types/d3-dsv": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.1.tgz", - "integrity": "sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA==" - }, - "@types/d3-ease": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-1.0.11.tgz", - "integrity": "sha512-wUigPL0kleGZ9u3RhzBP07lxxkMcUjL5IODP42mN/05UNL+JJCDnpEPpFbJiPvLcTeRKGIRpBBJyP/1BNwYsVA==" - }, - "@types/d3-fetch": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-1.2.2.tgz", - "integrity": "sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw==", - "requires": { - "@types/d3-dsv": "^1" - } - }, - "@types/d3-force": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz", - "integrity": "sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w==" - }, - "@types/d3-format": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz", - "integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==" - }, - "@types/d3-geo": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.3.tgz", - "integrity": "sha512-yZbPb7/5DyL/pXkeOmZ7L5ySpuGr4H48t1cuALjnJy5sXQqmSSAYBiwa6Ya/XpWKX2rJqGDDubmh3nOaopOpeA==", - "requires": { - "@types/geojson": "*" - } - }, - "@types/d3-hierarchy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", - "integrity": "sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg==" - }, - "@types/d3-interpolate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz", - "integrity": "sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==", - "requires": { - "@types/d3-color": "^1" - } - }, - "@types/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==" - }, - "@types/d3-polygon": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-1.0.8.tgz", - "integrity": "sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw==" - }, - "@types/d3-quadtree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz", - "integrity": "sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA==" - }, - "@types/d3-random": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-1.1.3.tgz", - "integrity": "sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ==" - }, - "@types/d3-scale": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.2.6.tgz", - "integrity": "sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw==", - "requires": { - "@types/d3-time": "^1" - } - }, - "@types/d3-scale-chromatic": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.1.tgz", - "integrity": "sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg==" - }, - "@types/d3-selection": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.3.tgz", - "integrity": "sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA==" - }, - "@types/d3-shape": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz", - "integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==", - "requires": { - "@types/d3-path": "^1" - } - }, - "@types/d3-time": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz", - "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==" - }, - "@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" - }, - "@types/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg==" - }, - "@types/d3-transition": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.3.2.tgz", - "integrity": "sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg==", - "requires": { - "@types/d3-selection": "^1" - } - }, - "@types/d3-voronoi": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz", - "integrity": "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==" - }, - "@types/d3-zoom": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.8.3.tgz", - "integrity": "sha512-3kHkL6sPiDdbfGhzlp5gIHyu3kULhtnHTTAl3UBZVtWB1PzcLL8vdmz5mTx7plLiUqOA2Y+yT2GKjt/TdA2p7Q==", - "requires": { - "@types/d3-interpolate": "^1", - "@types/d3-selection": "^1" - } - }, - "@types/geojson": { - "version": "7946.0.10", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" - }, "@types/jquery": { "version": "3.5.14", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", diff --git a/package.json b/package.json index 208095c75..eb1c7b71d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "dependencies": { "@types/bootstrap": "^5.1.4", "@types/bootstrap-notify": "^3.1.34", - "@types/d3": "^5.7.2", "@types/jquery": "^3.3.38", "@types/node": "^14.0.11", "@types/showdown": "^1.9.4", diff --git a/resources/call_split.svg b/resources/call_split.svg index eca29210e..f1e8aaec8 100644 --- a/resources/call_split.svg +++ b/resources/call_split.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/comment.svg b/resources/comment.svg index d471af342..6c022308c 100644 --- a/resources/comment.svg +++ b/resources/comment.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/description.svg b/resources/description.svg index 79bb59738..1201f5ef0 100644 --- a/resources/description.svg +++ b/resources/description.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/group.svg b/resources/group.svg index efcc5919e..7919378d1 100644 --- a/resources/group.svg +++ b/resources/group.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/merge_type.svg b/resources/merge_type.svg index bc5bf6a3b..cba088b30 100644 --- a/resources/merge_type.svg +++ b/resources/merge_type.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/play_arrow.svg b/resources/play_arrow.svg index 178bd3a4d..0772333b7 100644 --- a/resources/play_arrow.svg +++ b/resources/play_arrow.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/python.svg b/resources/python.svg index edc7c6d7f..38ad77a69 100644 --- a/resources/python.svg +++ b/resources/python.svg @@ -1 +1,16 @@ - \ No newline at end of file + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/question_mark.svg b/resources/question_mark.svg index 46b0deeba..70e8d9a3d 100644 --- a/resources/question_mark.svg +++ b/resources/question_mark.svg @@ -1,18 +1,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/share.svg b/resources/share.svg index 855763d8d..b4960e701 100644 --- a/resources/share.svg +++ b/resources/share.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/stop.svg b/resources/stop.svg index 937313642..0ef781f78 100644 --- a/resources/stop.svg +++ b/resources/stop.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/resources/tune.svg b/resources/tune.svg index ad36d1a72..344b68bff 100644 --- a/resources/tune.svg +++ b/resources/tune.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/src/Action.ts b/src/Action.ts new file mode 100644 index 000000000..de515f70b --- /dev/null +++ b/src/Action.ts @@ -0,0 +1,71 @@ +export class ActionMessage { + level: ActionMessage.Level; + message: string; + show: () => void; + fix: () => void; + fixDescription: string; + + constructor (level: ActionMessage.Level, message: string, show: () => void, fix: () => void, fixDescription: string){ + this.level = level; + this.message = message; + this.show = show; + this.fix = fix; + this.fixDescription = fixDescription; + } + + static Error(message: string): ActionMessage { + return new ActionMessage(ActionMessage.Level.Error, message, null, null, ""); + } + static Message(level: ActionMessage.Level, message: string): ActionMessage { + return new ActionMessage(level, message, null, null, ""); + } + static Show(level: ActionMessage.Level, message: string, show: () => void): ActionMessage { + return new ActionMessage(level, message, show, null, ""); + } + static Fix(level: ActionMessage.Level, message: string, fix: () => void, fixDescription: string): ActionMessage { + return new ActionMessage(level, message, null, fix, fixDescription); + } + static ShowFix(level: ActionMessage.Level, message: string, show: () => void, fix: () => void, fixDescription: string): ActionMessage { + return new ActionMessage(level, message, show, fix, fixDescription); + } + + // sorting order + // 1. by level + // 2. alphabetically by message + public static actionMessageSortFunc(a : ActionMessage, b : ActionMessage) : number { + if (a.level < b.level) + return -1; + + if (a.level > b.level) + return 1; + + if (a.message < b.message) + return -1; + + if (a.message > b.message) + return 1; + + return 0; + } + + // TODO: more cases? + public static levelToCss(level: ActionMessage.Level) : "danger" | "warning" | "info" | "success" { + switch (level){ + case ActionMessage.Level.Error: + return "danger"; + case ActionMessage.Level.Warning: + return "warning"; + default: + return "info"; + } + } +} + +export namespace ActionMessage { + export enum Level { + Success = "Success", + Error = "Error", + Warning = "Warning", + Info = "Info" + } +} \ No newline at end of file diff --git a/src/ActionList.ts b/src/ActionList.ts new file mode 100644 index 000000000..70a454281 --- /dev/null +++ b/src/ActionList.ts @@ -0,0 +1,193 @@ +import * as ko from "knockout"; + +import { ActionMessage } from "./Action"; +import { ComponentUpdater } from "./ComponentUpdater"; +import { Eagle } from './Eagle'; +import { Utils } from './Utils'; + +export class ActionList { + + mode: ko.Observable; + messages: ko.ObservableArray; + + constructor(){ + this.mode = ko.observable(ActionList.Mode.None) + this.messages = ko.observableArray([]); + } + + static perform = (index: number): void => { + console.log("ActionList.perform()", index); + + const eagle: Eagle = Eagle.getInstance(); + const actionList: ActionList = eagle.actionList(); + + const message = actionList.messages()[index]; + if (message.fix !== null){ + // perform the action + message.fix(); + + // and remove it from the list + actionList.messages.splice(index, 1); + } + + this.postPerformFunc(eagle); + } + + static performAll = () : void => { + console.log("performAll()"); + + const eagle: Eagle = Eagle.getInstance(); + const actionList: ActionList = eagle.actionList(); + const initialNumMessages = actionList.messages().length; + let numMessages = Infinity; + let numIterations = 0; + + // iterate through the messages list multiple times, until the length of the list is unchanged + while (numMessages !== actionList.messages().length){ + // check that we haven't iterated through the list too many times + if (numIterations > 10){ + console.warn("Too many iterations in performAll()"); + break; + } + numIterations = numIterations+1; + + numMessages = actionList.messages().length; + + for (let i = numMessages - 1 ; i >= 0 ; i--){ + const message = actionList.messages()[i]; + if (message.fix !== null){ + // perform the action + message.fix(); + + // and remove it from the list + actionList.messages.splice(i, 1); + } + } + } + + // show notification + Utils.showNotification("Performed All Actions: ", initialNumMessages + " action(s), " + numMessages + " remain. ", "info"); + + this.postPerformFunc(eagle); + } + + static postPerformFunc = (eagle: Eagle) => { + eagle.selectedObjects.valueHasMutated(); + eagle.logicalGraph().fileInfo().modified = true; + + switch (eagle.actionList().mode()){ + case ActionList.Mode.UpdateComponents: + ComponentUpdater.determineUpdates(eagle.palettes(), eagle.logicalGraph(), function(errors: ActionMessage[], updates: ActionMessage[]){ + this.messages(errors.concat(updates)); + }); + break; + case ActionList.Mode.Loading: + break; + } + + eagle.undo().pushSnapshot(eagle, "Performed Action(s)"); + } + + getNumPerformableActions : ko.PureComputed = ko.pureComputed(() => { + let count: number = 0; + + // count the warnings + for (const message of this.messages()){ + if (message.fix !== null){ + count += 1; + } + } + + return count; + }, this); + + + getNumWarnings : ko.PureComputed = ko.pureComputed(() => { + let result: number = 0; + + for (const error of this.messages()){ + if (error.level === ActionMessage.Level.Warning){ + result += 1; + } + } + + return result; + + }, this); + + getNumErrors : ko.PureComputed = ko.pureComputed(() => { + let result: number = 0; + + for (const error of this.messages()){ + if (error.level === ActionMessage.Level.Error){ + result += 1; + } + } + + return result; + }, this); + + getNumInfo : ko.PureComputed = ko.pureComputed(() => { + let result: number = 0; + + for (const error of this.messages()){ + if (error.level === ActionMessage.Level.Info){ + result += 1; + } + } + + return result; + }, this); + + static hasWarnings = (errors: ActionMessage[]) : boolean => { + if (errors === null){ + return false; + } + + for (const error of errors){ + if (error.level === ActionMessage.Level.Warning){ + return true; + } + } + return false; + } + + static hasErrors = (errors: ActionMessage[]) : boolean => { + if (errors === null){ + return false; + } + + for (const error of errors){ + if (error.level === ActionMessage.Level.Error){ + return true; + } + } + return false; + } + + // only update result if it is worse that current result + static worstError(errors: ActionMessage[]) : Eagle.LinkValid { + // TODO: can probably avoid doing two loops here! + const hasWarnings: boolean = ActionList.hasWarnings(errors); + const hasErrors: boolean = ActionList.hasErrors(errors); + + if (!hasWarnings && !hasErrors){ + return Eagle.LinkValid.Valid; + } + + if (hasErrors){ + return Eagle.LinkValid.Invalid; + } + + return Eagle.LinkValid.Warning; + } +} + +export namespace ActionList +{ + export enum Mode { + None = "None", + Loading = "Loading", + UpdateComponents = "UpdateComponents" + } +} diff --git a/src/Category.ts b/src/Category.ts index e111d89a3..6fabe4aa1 100644 --- a/src/Category.ts +++ b/src/Category.ts @@ -47,8 +47,7 @@ export namespace Category { export type CategoryData = { categoryType: Type, - isResizable:boolean, - canContainComponents:boolean, + isGroup:boolean, minInputs: number, maxInputs: number, minOutputs: number, @@ -80,11 +79,11 @@ export namespace Category { export enum Color { Application = "#0059a5", - Control = "rgb(88 167 94)", + Control = "rgb(46 161 55)", Data = "#2c2c2c", Description = "rgb(157 43 96)", Error = "#FF66CC", - Group = "rgb(211 165 0)", + Group = "rgb(227 189 100)", Object = "#00bfa6", Service = "purple" } diff --git a/src/CategoryData.ts b/src/CategoryData.ts index 933c3c4b1..51376afbf 100644 --- a/src/CategoryData.ts +++ b/src/CategoryData.ts @@ -2,53 +2,52 @@ import { Category } from './Category'; export class CategoryData { static readonly cData : {[category:string] : Category.CategoryData} = { - Branch : {categoryType: Category.Type.Control, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 2, maxOutputs: 2, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-branch", color: Category.Color.Control, collapsedHeaderOffsetY: 20, expandedHeaderOffsetY: 54, sortOrder: Category.SortOrder.Control}, - ExclusiveForceNode : {categoryType: Category.Type.Control, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-force_node", color: Category.Color.Control, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Control}, + Branch : {categoryType: Category.Type.Control, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 2, maxOutputs: 2, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-branch", color: Category.Color.Control, collapsedHeaderOffsetY: 20, expandedHeaderOffsetY: 54, sortOrder: Category.SortOrder.Control}, + ExclusiveForceNode : {categoryType: Category.Type.Control, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-force_node", color: Category.Color.Control, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Control}, - Comment : {categoryType: Category.Type.Other, isResizable: true, canContainComponents: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-comment", color: Category.Color.Description, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Documentation}, - Description : {categoryType: Category.Type.Other, isResizable: true, canContainComponents: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-paragraph-justify", color: Category.Color.Description, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Documentation}, - - Scatter : {categoryType: Category.Type.Construct, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-scatter", color: Category.Color.Group, collapsedHeaderOffsetY: 20, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, - Gather : {categoryType: Category.Type.Construct, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-gather", color: Category.Color.Group, collapsedHeaderOffsetY: 20, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, - MKN : {categoryType: Category.Type.Construct, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: true, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-many-to-many", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, - GroupBy : {categoryType: Category.Type.Construct, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: true, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-group", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, - Loop : {categoryType: Category.Type.Construct, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-loop", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, - SubGraph : {categoryType: Category.Type.Construct, isResizable: true, canContainComponents: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-sub_graph", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, + Comment : {categoryType: Category.Type.Other, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-comment", color: Category.Color.Description, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Documentation}, + + Scatter : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-scatter", color: Category.Color.Group, collapsedHeaderOffsetY: 20, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, + Gather : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-gather", color: Category.Color.Group, collapsedHeaderOffsetY: 20, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, + MKN : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: true, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-many-to-many", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, + GroupBy : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: true, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-group", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, + Loop : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: true, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-loop", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, + SubGraph : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-sub_graph", color: Category.Color.Group, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Construct}, - PythonApp : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python", color: Category.Color.Application, collapsedHeaderOffsetY: 10, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - PyFuncApp : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python", color: Category.Color.Application, collapsedHeaderOffsetY: 10, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - BashShellApp : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-bash", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - DynlibApp : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - DynlibProcApp : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - Mpi : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-mpi", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - Docker : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-docker", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - Singularity : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-singularity", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - UnknownApplication : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, + PythonApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python", color: Category.Color.Application, collapsedHeaderOffsetY: 10, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + PyFuncApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python", color: Category.Color.Application, collapsedHeaderOffsetY: 10, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + BashShellApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-bash", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + DynlibApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + DynlibProcApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + Mpi : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-mpi", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + Docker : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-docker", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + Singularity : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-singularity", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + UnknownApplication : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, - PythonMemberFunction : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_member_function", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - PythonObject : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_object", color: Category.Color.Object, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Object}, - DynlibMemberFunction : {categoryType: Category.Type.Application, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_member_function", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, - DynlibObject : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_object", color: Category.Color.Object, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Object}, + PythonMemberFunction : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_member_function", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + PythonObject : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_object", color: Category.Color.Object, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Object}, + DynlibMemberFunction : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_member_function", color: Category.Color.Application, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Application}, + DynlibObject : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_object", color: Category.Color.Object, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Object}, - File : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-description", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - Memory : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 1, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-memory", color: Category.Color.Data, collapsedHeaderOffsetY: 16, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - SharedMemory : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 1, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-shared_memory", color: Category.Color.Data, collapsedHeaderOffsetY: 16, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - NGAS : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-ngas", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - S3 : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-s3_bucket", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - ParameterSet : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - EnvironmentVariables : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - Data : {categoryType: Category.Type.Data, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-hard-drive", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + File : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-description", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + Memory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 1, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-memory", color: Category.Color.Data, collapsedHeaderOffsetY: 16, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + SharedMemory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 1, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-shared_memory", color: Category.Color.Data, collapsedHeaderOffsetY: 16, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + NGAS : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-ngas", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + S3 : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-s3_bucket", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + ParameterSet : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + EnvironmentVariables : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, + Data : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-hard-drive", color: Category.Color.Data, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Data}, - Plasma : {categoryType: Category.Type.Service, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasma", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, - PlasmaFlight : {categoryType: Category.Type.Service, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasmaflight", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, - RDBMS : {categoryType: Category.Type.Service, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, - Service : {categoryType: Category.Type.Service, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, + Plasma : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasma", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, + PlasmaFlight : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasmaflight", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, + RDBMS : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, + Service : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service, collapsedHeaderOffsetY: 4, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Service}, - Unknown : {categoryType: Category.Type.Unknown, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, - None : {categoryType: Category.Type.Unknown, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, + Unknown : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, + None : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, // legacy - Component : {categoryType: Category.Type.Unknown, isResizable: false, canContainComponents: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, + Component : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveInputApplication: false, canHaveOutputApplication: false, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-none", color: Category.Color.Error, collapsedHeaderOffsetY: 0, expandedHeaderOffsetY: 20, sortOrder: Category.SortOrder.Other}, }; @@ -59,8 +58,8 @@ export class CategoryData { console.error("Could not fetch category data for category", category); return { categoryType: Category.Type.Unknown, - isResizable: false, - canContainComponents: false, + + isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, diff --git a/src/Colors.ts b/src/Colors.ts new file mode 100644 index 000000000..6d84d633e --- /dev/null +++ b/src/Colors.ts @@ -0,0 +1,42 @@ +const colors: { name: string; color: string; }[] = [ + { + name: 'body', + color: '#2e3192' + },{ + name: 'graphText', + color: 'black' + },{ + name: 'nodeInputPort', + color: '#2bb673' + },{ + name: 'nodeOutputPort', + color: '#fbb040' + } +] + +const normalNodeRadius = 50 +const branchNodeRadius = 90 + +export class GraphConfig { + + static getColor = (name:string) : string => { + let result = 'red' + for (var color of colors) { + if(color.name === name){ + result = color.color + }else{ + continue + } + } + return result + } + + static getNormalRadius = () : number => { + return normalNodeRadius + } + + static getBranchRadius = () : number => { + return branchNodeRadius + } + +} \ No newline at end of file diff --git a/src/ComponentUpdater.ts b/src/ComponentUpdater.ts index 353990059..ba85f1d0c 100644 --- a/src/ComponentUpdater.ts +++ b/src/ComponentUpdater.ts @@ -1,52 +1,68 @@ -import {LogicalGraph} from './LogicalGraph'; -import {Palette} from './Palette'; -import {Node} from './Node'; -import {Utils} from './Utils'; -import {Errors} from './Errors'; +import { ActionMessage } from './Action'; +import { Eagle } from './Eagle'; +import { LogicalGraph } from './LogicalGraph'; +import { Node } from './Node'; +import { Palette } from './Palette'; +import { Utils } from './Utils'; + export class ComponentUpdater { - static update(palettes: Palette[], graph: LogicalGraph, callback : (errorsWarnings : Errors.ErrorsWarnings, updatedNodes : Node[]) => void) : void { - const errorsWarnings: Errors.ErrorsWarnings = {errors: [], warnings: []}; - const updatedNodes: Node[] = []; + static nodeMatchesPrototype = (node: Node, prototype: Node) : boolean => { + return node.getRepositoryUrl() !== "" && + prototype.getRepositoryUrl() !== "" && + node.getRepositoryUrl() === prototype.getRepositoryUrl() && + node.getName() === prototype.getName() && + node.getCommitHash() !== prototype.getCommitHash(); + } + + static determineUpdates(palettes: Palette[], graph: LogicalGraph, callback : (errors: ActionMessage[], updates : ActionMessage[]) => void) : void { + const errors: ActionMessage[] = []; + const updates: ActionMessage[] = []; // check if any nodes to update if (graph.getNodes().length === 0){ // TODO: don't showNotification here! instead add a warning to the errorsWarnings and callback() - errorsWarnings.errors.push(Errors.Message("Graph contains no components to update")); - callback(errorsWarnings, updatedNodes); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Graph contains no components to update")); + callback(errors, updates); return; } // make sure we have a palette available for each component in the graph for (let i = 0 ; i < graph.getNodes().length ; i++){ const node: Node = graph.getNodes()[i]; - let newVersion : Node = null; + let foundPrototype: boolean = false; for (const palette of palettes){ for (const paletteNode of palette.getNodes()){ - if (Node.requiresUpdate(node, paletteNode)){ - newVersion = paletteNode; + + if (!foundPrototype && ComponentUpdater.nodeMatchesPrototype(node, paletteNode)){ + foundPrototype = true; + const nodeUpdates : ActionMessage[] = ComponentUpdater.nodeDetermineUpdates(node, paletteNode); + updates.push(...nodeUpdates); } } } - if (newVersion === null){ - console.log("No match for node", node.getName()); - errorsWarnings.warnings.push(Errors.Message("Could not find appropriate palette for node " + node.getName() + " from repository " + node.getRepositoryUrl())); + if (!foundPrototype){ + //console.log("No match for node", node.getName()); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Could not find appropriate palette to use as prototype for " + node.getName() + " component.")); continue; } // update the node with a new definition - ComponentUpdater._replaceNode(node, newVersion); - updatedNodes.push(node); + //ComponentUpdater._replaceNode(node, newVersion); + //updatedNodes.push(node); } - callback(errorsWarnings, updatedNodes); + callback(errors, updates); } // NOTE: the replacement here is "additive", any fields missing from the old node will be added, but extra fields in the old node will not removed - static _replaceNode(dest:Node, src:Node){ + static nodeDetermineUpdates(dest:Node, src:Node) : ActionMessage[] { + const eagle = Eagle.getInstance(); + const updates: ActionMessage[] = []; + for (let i = 0 ; i < src.getFields().length ; i++){ const srcField = src.getFields()[i]; @@ -60,12 +76,36 @@ export class ComponentUpdater { // if dest field could not be found, then go ahead and add a NEW field to the dest node if (destField === null){ - destField = srcField.clone(); - dest.addField(destField); + const newField = srcField.clone(); + //dest.addField(destField); + updates.push( + ActionMessage.ShowFix( + ActionMessage.Level.Info, + dest.getName() + " (" + dest.getKey() + ") component is missing a '" + srcField.getDisplayText() + "' field", + function(){Utils.showNode(eagle, Eagle.FileType.Graph, dest.getId())}, + function(){Utils.fixNodeAddField(dest, newField)}, + "Add '" + srcField.getDisplayText() + "' field to " + dest.getName() + " component" + ) + ); + } + + if (destField !== null){ + if (destField.getValue() !== srcField.getValue()){ + updates.push( + ActionMessage.ShowFix( + ActionMessage.Level.Info, + dest.getName() + " (" + dest.getKey() + ") component '" + srcField.getDisplayText() + "' field has different value", + function(){Utils.showNode(eagle, Eagle.FileType.Graph, dest.getId())}, + function(){Utils.fixFieldValue(dest, srcField, srcField.getValue())}, + "Update " + dest.getName() + " (" + dest.getKey() + ") field '" + srcField.getDisplayText() + "' from " + destField.getValue() + " to " + srcField.getValue() + ) + ); + } + + // TODO: check other differences between the destField and srcField } - - // copy everything about the field from the src (palette), except maintain the existing id and nodeKey - destField.copyWithKeyAndId(srcField, destField.getNodeKey(), destField.getId()); } + + return updates; } } diff --git a/src/Config.ts b/src/Config.ts index ab1cc0b7d..1920cb8d3 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -34,6 +34,4 @@ export class Config { static readonly HIERARCHY_EDGE_SELECTED_COLOR : string = "rgb(47 22 213)"; static readonly HIERARCHY_EDGE_DEFAULT_COLOR : string = "black"; - - static readonly SELECTED_NODE_COLOR : string = "rgb(47 22 213)"; } diff --git a/src/Daliuge.ts b/src/Daliuge.ts index 7dfcd534c..c2fdab5f4 100644 --- a/src/Daliuge.ts +++ b/src/Daliuge.ts @@ -66,7 +66,10 @@ export namespace Daliuge { // branch YES = "yes", - NO = "no" + NO = "no", + + // dummy + DUMMY = "dummy" } export enum DataType { @@ -110,6 +113,18 @@ export namespace Daliuge { OJS = "OJS", } + export enum ReproducibilityMode { + Nothing = 0, + All = -1, + Rerun = 1, + Repeat = 2, + Recompute = 4, + Reproduce = 5, + Replicate_Sci = 6, + Replicate_Comp = 7, + Replicate_Totally = 8 + } + // These are the canonical example definition of each field export const groupStartField = new Field("", FieldName.GROUP_START, "true", "true", "", false, DataType.Boolean, false, [], false, FieldType.ComponentParameter, FieldUsage.NoPort, false); export const groupEndField = new Field("", FieldName.GROUP_END, "true", "true", "", false, DataType.Boolean, false, [], false, FieldType.ComponentParameter, FieldUsage.NoPort, false); @@ -134,8 +149,7 @@ export namespace Daliuge { { categoryTypes: [ Category.Type.Application, - Category.Type.Data, - Category.Type.Construct + Category.Type.Data ], fields: [ Daliuge.dropClassField diff --git a/src/Eagle.ts b/src/Eagle.ts index 0e3567ab8..365fc0db2 100644 --- a/src/Eagle.ts +++ b/src/Eagle.ts @@ -36,6 +36,7 @@ import {Translator} from './Translator'; import {Category} from './Category'; import {CategoryData} from './CategoryData'; import {Daliuge} from './Daliuge'; +import { GraphConfig } from "./graphConfig"; import {UiMode, UiModeSystem, SettingData} from './UiModes'; import {LogicalGraph} from './LogicalGraph'; @@ -51,10 +52,13 @@ import {SideWindow} from './SideWindow'; import {ExplorePalettes} from './ExplorePalettes'; import {Hierarchy} from './Hierarchy'; import {Undo} from './Undo'; -import {Errors} from './Errors'; import {ComponentUpdater} from './ComponentUpdater'; import {ParameterTable} from './ParameterTable'; +import { ActionList } from "./ActionList"; +import { ActionMessage } from "./Action"; import { RightClick } from "./RightClick"; +import { GraphChecker } from "./GraphChecker"; +import { GraphRenderer } from "./GraphRenderer"; export class Eagle { static _instance : Eagle; @@ -79,9 +83,9 @@ export class Eagle { undo : ko.Observable; parameterTable : ko.Observable; - globalOffsetX : number; - globalOffsetY : number; - globalScale : number; + globalOffsetX : ko.Observable; + globalOffsetY : ko.Observable; + globalScale : ko.Observable; quickActionSearchTerm : ko.Observable; quickActionOpen : ko.Observable; @@ -92,12 +96,14 @@ export class Eagle { rendererFrameCountTick : number; explorePalettes : ko.Observable; + actionList : ko.Observable; + graphChecker : ko.Observable; + + // TODO: move these to GraphRenderer.ts + isDragging : ko.Observable; + draggingNode : ko.Observable; errorsMode : ko.Observable; - graphWarnings : ko.ObservableArray; - graphErrors : ko.ObservableArray; - loadingWarnings : ko.ObservableArray; - loadingErrors : ko.ObservableArray; tableModalType : ko.Observable; showTableModal : ko.Observable; currentFileInfo : ko.Observable; @@ -164,9 +170,9 @@ export class Eagle { Eagle.nodeDragPaletteIndex = null; Eagle.nodeDragComponentIndex = null; - this.globalOffsetX = 0; - this.globalOffsetY = 0; - this.globalScale = 1.0; + this.globalOffsetX = ko.observable(0); + this.globalOffsetY = ko.observable(0); + this.globalScale = ko.observable(1.0); this.quickActionSearchTerm = ko.observable('') this.quickActionOpen = ko.observable(false) @@ -177,12 +183,12 @@ export class Eagle { this.rendererFrameCountTick = 0; this.explorePalettes = ko.observable(new ExplorePalettes()); + this.actionList = ko.observable(new ActionList()); + this.graphChecker = ko.observable(new GraphChecker()); + this.isDragging = ko.observable(false); + this.draggingNode = ko.observable(null); this.errorsMode = ko.observable(Setting.ErrorsMode.Loading); - this.graphWarnings = ko.observableArray([]); - this.graphErrors = ko.observableArray([]); - this.loadingWarnings = ko.observableArray([]); - this.loadingErrors = ko.observableArray([]); this.tableModalType = ko.observable('') this.showTableModal = ko.observable(false) @@ -194,7 +200,8 @@ export class Eagle { this.snapToGrid = ko.observable(false); this.selectedObjects.subscribe(function(){ - this.logicalGraph.valueHasMutated(); + //TODO check if the selectedObjects array has changed, if not, abort + GraphRenderer.nodeData = GraphRenderer.depthFirstTraversalOfNodes(this.logicalGraph(), this.showDataNodes()); Hierarchy.updateDisplay() if(this.selectedObjects().length === 0){ this.tableModalType('keyParametersTableModal') @@ -432,13 +439,12 @@ export class Eagle { } zoomIn = () : void => { - this.globalScale += 0.05; - this.logicalGraph.valueHasMutated(); + //changed the equations to make the speec a curve and prevent the graph from inverting + this.globalScale(Math.abs(this.globalScale() + this.globalScale()*0.2)); } zoomOut = () : void => { - this.globalScale -= 0.05; - this.logicalGraph.valueHasMutated(); + this.globalScale(Math.abs(this.globalScale() - this.globalScale()*0.2)); } zoomToFit = () : void => { @@ -450,8 +456,10 @@ export class Eagle { } centerGraph = () : void => { + const that = this + // if there are no nodes in the logical graph, abort - if (this.logicalGraph().getNumNodes() === 0){ + if (that.logicalGraph().getNumNodes() === 0){ return; } @@ -460,38 +468,83 @@ export class Eagle { let minY : number = Number.MAX_VALUE; let maxX : number = -Number.MAX_VALUE; let maxY : number = -Number.MAX_VALUE; - for (const node of this.logicalGraph().getNodes()){ - if (node.getPosition().x < minX){ - minX = node.getPosition().x; + for (const node of that.logicalGraph().getNodes()){ + if (node.getPosition().x - node.getRadius() < minX){ + minX = node.getPosition().x - node.getRadius(); } - if (node.getPosition().y < minY){ - minY = node.getPosition().y; + if (node.getPosition().y - node.getRadius() < minY){ + minY = node.getPosition().y - node.getRadius(); } - if (node.getPosition().x + node.getWidth() > maxX){ - maxX = node.getPosition().x + node.getWidth(); + if (node.getPosition().x + node.getRadius() > maxX){ + maxX = node.getPosition().x + node.getRadius(); } - if (node.getPosition().y + node.getHeight() > maxY){ - maxY = node.getPosition().y + node.getHeight(); + if (node.getPosition().y + node.getRadius() > maxY){ + maxY = node.getPosition().y + node.getRadius(); } } - // determine the centroid of the graph const centroidX = minX + ((maxX - minX) / 2); const centroidY = minY + ((maxY - minY) / 2); + - // reset scale - this.globalScale = 1.0; + //calculating scale multipliers needed for each, height and width in order to fit the graph + const containerHeight = $('#logicalGraphParent').height() + const graphHeight = maxY-minY+200 + const graphYScale = containerHeight/graphHeight + + + //we are taking into account the current widths of the left and right windows + let leftWindow = 0 + if(that.leftWindow().shown()){ + leftWindow = that.leftWindow().width() + } + + let rightWindow = 0 + if(that.rightWindow().shown()){ + rightWindow = that.rightWindow().width() + } + + const containerWidth = $('#logicalGraphParent').width() - leftWindow - rightWindow + const graphWidth = maxX-minX+200 + const graphXScale = containerWidth/graphWidth + + // reset scale to center the graph correctly + that.globalScale(1) //determine center of the display area - const displayCenterX : number = $('#logicalGraphParent').width() / this.globalScale / 2; - const displayCenterY : number = $('#logicalGraphParent').height() / this.globalScale / 2; + const displayCenterX : number = (containerWidth / that.globalScale() / 2); + const displayCenterY : number = $('#logicalGraphParent').height() / that.globalScale() / 2; // translate display to center the graph centroid - this.globalOffsetX = displayCenterX - centroidX; - this.globalOffsetY = displayCenterY - centroidY; + that.globalOffsetX(Math.round(displayCenterX - centroidX + leftWindow)); + that.globalOffsetY(Math.round(displayCenterY - centroidY)); + + //taking note of the screen center in graph space before zooming + const midpointx = $('#logicalGraphParent').width()/2 + const midpointy = ($('#logicalGraphParent').height())/2 + const xpb = midpointx/that.globalScale() - that.globalOffsetX(); + const ypb = (midpointy)/that.globalScale() - that.globalOffsetY(); + + //applying the correct zoom + if(graphYScale>graphXScale){ + that.globalScale(graphXScale); + }else if(graphYScale { @@ -564,12 +617,18 @@ export class Eagle { setSelection = (rightWindowMode : Eagle.RightWindowMode, selection : Node | Edge, selectedLocation: Eagle.FileType) : void => { Eagle.selectedLocation(selectedLocation); + GraphRenderer.clearPortPeek() if (selection === null){ this.selectedObjects([]); this.rightWindow().mode(rightWindowMode); } else { this.selectedObjects([selection]); + //show the title of the port on either seide of the edge we are selecting + if(selection instanceof Edge){ + GraphRenderer.setPortPeekForEdge(selection,true) + } + //special case if we are selecting multiple things in a palette if(selectedLocation === Eagle.FileType.Palette){ this.hierarchyMode(false) @@ -615,11 +674,16 @@ export class Eagle { if (alreadySelected){ // remove this.selectedObjects.splice(index,1); + } else { // add this.selectedObjects.push(selection); } + if( selection instanceof Edge){ + GraphRenderer.setPortPeekForEdge(selection,!alreadySelected) + } + //special case if we are selecting multiple things in a palette if(selectedLocation === Eagle.FileType.Palette){ this.hierarchyMode(false) @@ -699,17 +763,26 @@ export class Eagle { this._loadGraphJSON(data, fileFullPath, (lg: LogicalGraph) : void => { this.logicalGraph(lg); + const eagle = this // center graph - this.centerGraph(); + GraphRenderer.translateLegacyGraph() + + //needed when centering after init of a graph. we need to wait for all the constructs to finish resizing themselves + setTimeout(function(){ + eagle.centerGraph() + console.log(eagle) + },50) // update the activeFileInfo with details of the repository the file was loaded from if (fileFullPath !== ""){ - this.updateLogicalGraphFileInfo(Eagle.RepositoryService.File, "", "", Utils.getFilePathFromFullPath(fileFullPath), Utils.getFileNameFromFullPath(fileFullPath)); + const repositoryFile = new RepositoryFile(new Repository(Eagle.RepositoryService.File, "", "", false), Utils.getFilePathFromFullPath(fileFullPath), Utils.getFileNameFromFullPath(fileFullPath)); + this.updateLogicalGraphFileInfo(repositoryFile); } // check graph - this.checkGraph(); + this.graphChecker().check(); + this.undo().clear(); this.undo().pushSnapshot(this, "Loaded " + fileFullPath); }); }); @@ -721,7 +794,7 @@ export class Eagle { insertGraphFile = () : void => { const uploadedGraphFileToInsertInputElement : HTMLInputElement = document.getElementById("uploadedGraphFileToInsert"); const fileFullPath : string = uploadedGraphFileToInsertInputElement.value; - const errorsWarnings : Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; + const errors: ActionMessage[] = []; // abort if value is empty string if (fileFullPath === ""){ @@ -742,33 +815,46 @@ export class Eagle { this._loadGraphJSON(data, fileFullPath, (lg: LogicalGraph) : void => { const parentNode: Node = new Node(Utils.newKey(this.logicalGraph().getNodes()), lg.fileInfo().name, lg.fileInfo().getText(), Category.SubGraph); - this.insertGraph(lg.getNodes(), lg.getEdges(), parentNode, errorsWarnings); + this.insertGraph(lg.getNodes(), lg.getEdges(), parentNode, errors); // TODO: handle errors and warnings - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Insert Logical Graph"); this.logicalGraph.valueHasMutated(); }); }); } - private _handleLoadingErrors = (errorsWarnings: Errors.ErrorsWarnings, fileName: string, service: Eagle.RepositoryService) : void => { + handleLoadingErrors = (loads: {file: RepositoryFile, errors: ActionMessage[]}[]) : void => { const showErrors: boolean = Setting.findValue(Setting.SHOW_FILE_LOADING_ERRORS); + // combine all the data in loads + const combinedErrors: {source: string, messages: ActionMessage[]}[] = []; + let combinedErrorCount: number = 0; + for (const load of loads){ + combinedErrors.push({source: load.file.name, messages: load.errors}); + combinedErrorCount += load.errors.length; + } + // show errors (if found) - if (Errors.hasErrors(errorsWarnings) || Errors.hasWarnings(errorsWarnings)){ + if (combinedErrorCount > 0){ if (showErrors){ - // add warnings/errors to the arrays - this.loadingErrors(errorsWarnings.errors); - this.loadingWarnings(errorsWarnings.warnings); - - this.errorsMode(Setting.ErrorsMode.Loading); - Utils.showErrorsModal("Loading File"); + Utils.showActionListModal("Loading File(s)", ActionList.Mode.Loading, combinedErrors); } } else { - Utils.showNotification("Success", fileName + " has been loaded from " + service + ".", "success"); + const messages: string[] = []; + + for (const load of loads){ + if (load.file.repository.service === Eagle.RepositoryService.Unknown){ + messages.push(load.file.name + " has been loaded.", "success"); + } else { + messages.push(load.file.name + " has been loaded from " + load.file.repository.service + "."); + } + } + + Utils.showNotification("Success", messages.join('
'), "success"); } } @@ -795,7 +881,7 @@ export class Eagle { // attempt to determine schema version from FileInfo const schemaVersion: Daliuge.SchemaVersion = Utils.determineSchemaVersion(dataObject); - const errorsWarnings: Errors.ErrorsWarnings = {errors: [], warnings: []}; + const errorsWarnings: ActionMessage[] = []; const dummyFile: RepositoryFile = new RepositoryFile(Repository.DUMMY, "", fileFullPath); // use the correct parsing function based on schema version @@ -806,7 +892,7 @@ export class Eagle { break; } - this._handleLoadingErrors(errorsWarnings, Utils.getFileNameFromFullPath(fileFullPath), Eagle.RepositoryService.File); + this.handleLoadingErrors([{file: dummyFile, errors: errorsWarnings}]); } createSubgraphFromSelection = () : void => { @@ -842,13 +928,15 @@ export class Eagle { // flag graph as changed this.flagActiveFileModified(); - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Create Subgraph from Selection"); this.logicalGraph.valueHasMutated(); } - checkErrorModalShowError = (data:any) :void =>{ - data.show() + showError = (data:ActionMessage) :void =>{ + // hide modal? + + data.show(); this.rightWindow().shown(true).mode(Eagle.RightWindowMode.Inspector) } @@ -888,14 +976,14 @@ export class Eagle { // flag graph as changed this.flagActiveFileModified(); - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Add Selection to Construct"); this.logicalGraph.valueHasMutated(); }); } // NOTE: parentNode would be null if we are duplicating a selection of objects - insertGraph = (nodes: Node[], edges: Edge[], parentNode: Node, errorsWarnings: Errors.ErrorsWarnings) : void => { + insertGraph = (nodes: Node[], edges: Edge[], parentNode: Node, errors: ActionMessage[]) : void => { const DUPLICATE_OFFSET: number = 20; // amount (in x and y) by which duplicated nodes will be positioned away from the originals // create map of inserted graph keys to final graph nodes, and of inserted port ids to final graph ports @@ -915,8 +1003,7 @@ export class Eagle { // set attributes of parentNode parentNode.setPosition(parentNodePosition.x, parentNodePosition.y); - parentNode.setWidth(bbSize.x); - parentNode.setHeight(bbSize.y); + parentNode.setRadius(Math.max(bbSize.x, bbSize.y)); parentNode.setCollapsed(true); } else { parentNodePosition = {x: DUPLICATE_OFFSET, y: DUPLICATE_OFFSET}; @@ -924,7 +1011,7 @@ export class Eagle { // insert nodes from lg into the existing logicalGraph for (const node of nodes){ - this.addNode(node.clone(), parentNodePosition.x + node.getPosition().x, parentNodePosition.y + node.getPosition().y, (insertedNode: Node) => { + this.addNode(node, parentNodePosition.x + node.getPosition().x, parentNodePosition.y + node.getPosition().y, (insertedNode: Node) => { // save mapping for node itself keyMap.set(node.getKey(), insertedNode); @@ -932,44 +1019,79 @@ export class Eagle { if (insertedNode.getParentKey() === null && parentNode !== null){ insertedNode.setParentKey(parentNode.getKey()); } - + // copy embedded input application if (node.hasInputApplication()){ - const inputApplication : Node = node.getInputApplication(); - const clone : Node = inputApplication.clone(); - const newKey : number = Utils.newKey(this.logicalGraph().getNodes()); - clone.setKey(newKey); - keyMap.set(inputApplication.getKey(), clone); + const oldInputApplication : Node = node.getInputApplication(); + const newInputApplication : Node = insertedNode.getInputApplication(); + console.log(insertedNode) + + + keyMap.set(oldInputApplication.getKey(), newInputApplication); - insertedNode.setInputApplication(clone); + // insertedNode.setInputApplication(newInputApplication); // loop through ports, adding them to the port map - for (const inputPort of inputApplication.getInputPorts()){ - portMap.set(inputPort.getId(), inputPort); + // for (const inputPort of oldInputApplication.getInputPorts()){ + // portMap.set(inputPort.getId(), newInputApplication); + // } + + // for (const outputPort of inputApplication.getOutputPorts()){ + // portMap.set(outputPort.getId(), outputPort); + // } + + console.log(node.hasInputApplication(), oldInputApplication,newInputApplication) + // save mapping for input ports + for (let j = 0 ; j < oldInputApplication.getInputPorts().length; j++ ){ + portMap.set(oldInputApplication.getInputPorts()[j].getId(), newInputApplication.getInputPorts()[j]); + } - for (const outputPort of inputApplication.getOutputPorts()){ - portMap.set(outputPort.getId(), outputPort); + // save mapping for output ports + for (let j = 0 ; j < oldInputApplication.getOutputPorts().length; j++){ + portMap.set(oldInputApplication.getOutputPorts()[j].getId(), newInputApplication.getOutputPorts()[j]); } } // copy embedded output application if (node.hasOutputApplication()){ - const outputApplication : Node = node.getOutputApplication(); - const clone : Node = outputApplication.clone(); - const newKey : number = Utils.newKey(this.logicalGraph().getNodes()); - clone.setKey(newKey); - keyMap.set(outputApplication.getKey(), clone); - - insertedNode.setOutputApplication(clone); + const oldOutputApplication : Node = node.getOutputApplication(); + const newOutputApplication : Node = insertedNode.getOutputApplication(); + // const clone : Node = outputApplication.clone(); + // const newKey : number = Utils.newKey(this.logicalGraph().getNodes()); + // const newId : string = Utils.uuidv4(); + // clone.setKey(newKey); + // clone.setId(newId); + + // if(clone.getFields() != null){ + // // set new ids for any fields in this node + // for (const field of clone.getFields()){ + // field.setId(Utils.uuidv4()); + // } + // } + + keyMap.set(oldOutputApplication.getKey(), newOutputApplication); + + + // insertedNode.setOutputApplication(clone); // loop through ports, adding them to the port map - for (const inputPort of outputApplication.getInputPorts()){ - portMap.set(inputPort.getId(), inputPort); + // for (const inputPort of outputApplication.getInputPorts()){ + // portMap.set(inputPort.getId(), inputPort); + // } + + // for (const outputPort of outputApplication.getOutputPorts()){ + // portMap.set(outputPort.getId(), outputPort); + // } + + // save mapping for input ports + for (let j = 0 ; j < oldOutputApplication.getInputPorts().length; j++){ + portMap.set(oldOutputApplication.getInputPorts()[j].getId(), newOutputApplication.getInputPorts()[j]); } - for (const outputPort of outputApplication.getOutputPorts()){ - portMap.set(outputPort.getId(), outputPort); + // save mapping for output ports + for (let j = 0 ; j < oldOutputApplication.getOutputPorts().length; j++){ + portMap.set(oldOutputApplication.getOutputPorts()[j].getId(), newOutputApplication.getOutputPorts()[j]); } } @@ -1017,7 +1139,7 @@ export class Eagle { const destNode = keyMap.get(edge.getDestNodeKey()); if (typeof srcNode === "undefined" || typeof destNode === "undefined"){ - errorsWarnings.warnings.push(Errors.Message("Unable to insert edge " + edge.getId() + " source node or destination node could not be found.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Unable to insert edge " + edge.getId() + " source node or destination node could not be found.")); continue; } @@ -1083,11 +1205,12 @@ export class Eagle { return; } - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; - const p : Palette = Palette.fromOJSJson(data, new RepositoryFile(Repository.DUMMY, "", Utils.getFileNameFromFullPath(fileFullPath)), errorsWarnings); + const errorsWarnings: ActionMessage[] = []; + const dummyFile = new RepositoryFile(new Repository(Eagle.RepositoryService.File, "", "", false), "", Utils.getFileNameFromFullPath(fileFullPath)); + const p : Palette = Palette.fromOJSJson(data, dummyFile, errorsWarnings); // show errors (if found) - this._handleLoadingErrors(errorsWarnings, Utils.getFileNameFromFullPath(fileFullPath), Eagle.RepositoryService.File); + this.handleLoadingErrors([{file: dummyFile, errors:errorsWarnings}]); // sort the palette p.sort(); @@ -1124,7 +1247,8 @@ export class Eagle { this.newDiagram(Eagle.FileType.Graph, (name: string) => { this.logicalGraph(new LogicalGraph()); this.logicalGraph().fileInfo().name = name; - this.checkGraph(); + this.graphChecker().check(); + this.undo().clear(); this.undo().pushSnapshot(this, "New Logical Graph"); this.logicalGraph.valueHasMutated(); Utils.showNotification("New Graph Created",name, "success"); @@ -1166,7 +1290,7 @@ export class Eagle { const nodes : Node[] = []; const edges : Edge[] = []; - const errorsWarnings : Errors.ErrorsWarnings = {"errors": [], "warnings": []}; + const errorsWarnings : ActionMessage[] = []; for (const n of clipboard.nodes){ const node = Node.fromOJSJson(n, null, false, (): number => { @@ -1191,7 +1315,7 @@ export class Eagle { // TODO: show errors // ensure changes are reflected in display - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Added from JSON"); this.logicalGraph.valueHasMutated(); }); @@ -1264,15 +1388,18 @@ export class Eagle { Repositories.selectFile(new RepositoryFile(new Repository(fileInfo.repositoryService, fileInfo.repositoryName, fileInfo.repositoryBranch, false), fileInfo.path, fileInfo.name)); break; case Eagle.RepositoryService.Url: + /* this.loadPalettes([ {name:palette.fileInfo().name, filename:palette.fileInfo().downloadUrl, readonly:palette.fileInfo().readonly} - ], (errorsWarnings: Errors.ErrorsWarnings, palettes: Palette[]):void => { + ], (errorsWarnings: ActionMessage[], palettes: Palette[]):void => { for (const palette of palettes){ if (palette !== null){ this.palettes.splice(index, 0, palette); } } }); + */ + console.warn("Not implemented!"); break; default: // can't be fetched @@ -1285,7 +1412,10 @@ export class Eagle { */ newDiagram = (fileType : Eagle.FileType, callbackAction : (name : string) => void ) : void => { console.log("newDiagram()", fileType); - Utils.requestUserString("New " + fileType, "Enter " + fileType + " name", "", false, (completed : boolean, userString : string) : void => { + + const defaultName: string = Utils.generateGraphName(); + + Utils.requestUserString("New " + fileType, "Enter " + fileType + " name", defaultName, false, (completed : boolean, userString : string) : void => { if (!completed) { // Cancelling action. return; @@ -1594,6 +1724,7 @@ export class Eagle { // clone the logical graph const lg_clone : LogicalGraph = ( obj).clone(); lg_clone.fileInfo().updateEagleInfo(); + const jsonString: string = LogicalGraph.toOJSJsonString(lg_clone, false); this._saveDiagramToGit(repository, fileType, filePath, fileName, fileInfo, commitMessage, jsonString); @@ -1633,68 +1764,62 @@ export class Eagle { } // validate json - if (!Setting.findValue(Setting.DISABLE_JSON_VALIDATION)){ - const jsonObject = JSON.parse(jsonString); - const validatorResult : {valid: boolean, errors: string} = Utils.validateJSON(jsonObject, Daliuge.SchemaVersion.OJS, fileType); - if (!validatorResult.valid){ - const message = "JSON Output failed validation against internal JSON schema, saving anyway"; - console.error(message, validatorResult.errors); - Utils.showUserMessage("Error", message + "
" + validatorResult.errors); - //return; - } - } + Utils.validateJSON(jsonString, fileType); const commitJsonString: string = Utils.createCommitJsonString(jsonString, repository, token, fullFileName, commitMessage); this.saveFileToRemote(repository, filePath, fileName, fileType, fileInfo, commitJsonString); } - loadPalettes = (paletteList: {name:string, filename:string, readonly:boolean}[], callback: (errorsWarnings: Errors.ErrorsWarnings, data: Palette[]) => void ) : void => { - const results: Palette[] = []; - const complete: boolean[] = []; - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; - - for (let i = 0 ; i < paletteList.length ; i++){ - results.push(null); - complete.push(false); - const index = i; - const data = {url: paletteList[i].filename}; + loadFiles = (files: RepositoryFile[], callback: (palettes: {file: RepositoryFile, palette: Palette, errors: ActionMessage[]}[], logicalGraphs: {file: RepositoryFile, logicalGraph: LogicalGraph, errors: ActionMessage[]}[]) => void ) : void => { + console.log("loadFiles()", files.length); + for (const file of files){ + console.log(" - ", file.name); + } - Utils.httpPostJSON("/openRemoteUrlFile", data, (error: string, data: string) => { - complete[index] = true; + const filesComplete: boolean[] = []; + const completedFiles: {file: RepositoryFile, fileType: Eagle.FileType, object: LogicalGraph | Palette, errors: ActionMessage[]}[] = []; - if (error !== null){ - console.error(error); - errorsWarnings.errors.push(Errors.Message(error)); - } else { - const palette: Palette = Palette.fromOJSJson(data, new RepositoryFile(Repository.DUMMY, "", paletteList[index].name), errorsWarnings); - palette.fileInfo().clear(); - palette.fileInfo().name = paletteList[index].name; - palette.fileInfo().readonly = paletteList[index].readonly; - palette.fileInfo().builtIn = true; - palette.fileInfo().downloadUrl = paletteList[index].filename; - palette.fileInfo().type = Eagle.FileType.Palette; - palette.fileInfo().repositoryService = Eagle.RepositoryService.Url; - - // sort palette and add to results - palette.sort(); - results[index] = palette; - } + for (let i = 0 ; i < files.length ; i++){ + filesComplete.push(false); + completedFiles.push(null); + const index = i; + + this.openRemoteFile(files[i], function(errors: ActionMessage[], file: RepositoryFile, fileTypeLoaded: Eagle.FileType, object: LogicalGraph | Palette){ + // mark file complete, add files to array + filesComplete[index] = true; + completedFiles[index] = {file: files[index], fileType: fileTypeLoaded, object: object, errors: errors}; // check if all requests are now complete, then we can call the callback let allComplete = true; - for (const requestComplete of complete){ + for (const requestComplete of filesComplete){ if (!requestComplete){ allComplete = false; } } + if (allComplete){ - callback(errorsWarnings, results); + const palettes: {file: RepositoryFile, palette: Palette, errors: ActionMessage[]}[] = []; + const logicalGraphs: {file: RepositoryFile, logicalGraph: LogicalGraph, errors: ActionMessage[]}[] = []; + + // add to the two arrays + for (const completedFile of completedFiles){ + switch(completedFile.fileType){ + case Eagle.FileType.Graph: + logicalGraphs.push({file: completedFile.file, logicalGraph: completedFile.object, errors: completedFile.errors}); + break; + case Eagle.FileType.Palette: + palettes.push({file: completedFile.file, palette: completedFile.object, errors: completedFile.errors}); + break; + } + } + + callback(palettes, logicalGraphs); } }); } } - openRemoteFile = (file : RepositoryFile) : void => { + openRemoteFile = (file : RepositoryFile, callback: (errors: ActionMessage[], file: RepositoryFile, fileTypeLoaded: Eagle.FileType, object: LogicalGraph | Palette) => void) : void => { // flag file as being fetched file.isFetching(true); @@ -1716,7 +1841,7 @@ export class Eagle { } // load file from github or gitlab - openRemoteFileFunc(file.repository.service, file.repository.name, file.repository.branch, file.path, file.name, (error : string, data : string) : void => { + openRemoteFileFunc(file, (error : string, data : string) : void => { // flag fetching as complete file.isFetching(false); @@ -1724,83 +1849,93 @@ export class Eagle { if (error != null){ Utils.showUserMessage("Error", error); console.error(error); + if (callback !== null) callback([ActionMessage.Error(error)], file, file.type, null); return; } // determine file extension const fileExtension = Utils.getFileExtension(file.name); - let fileTypeLoaded: Eagle.FileType = Eagle.FileType.Unknown; - let dataObject: any = null; - if (fileExtension !== "md"){ - // attempt to parse the JSON - try { - dataObject = JSON.parse(data); - } - catch(err){ - Utils.showUserMessage("Error parsing file JSON", err.message); - return; - } + if (fileExtension === "md"){ + this._load(Eagle.FileType.Markdown, null, data, file, callback); + return; + } - fileTypeLoaded = Utils.determineFileType(dataObject); - // console.log("fileTypeLoaded", fileTypeLoaded); - } else { - fileTypeLoaded = Eagle.FileType.Markdown; - } - - switch (fileTypeLoaded){ - case Eagle.FileType.Graph: - // attempt to determine schema version from FileInfo - const eagleVersion: string = Utils.determineEagleVersion(dataObject); - - // warn user if file newer than EAGLE - if (Utils.newerEagleVersion(eagleVersion, (window).version)){ - Utils.requestUserConfirm("Newer EAGLE Version", "File " + file.name + " was written with EAGLE version " + eagleVersion + ", whereas the current EAGLE version is " + (window).version + ". Do you wish to load the file anyway?", "Yes", "No", "", (confirmed : boolean) : void => { - if (confirmed){ - this._loadGraph(dataObject, file); - } - }); - } else { - this._loadGraph(dataObject, file); - } - break; + // attempt to parse the JSON + let dataObject: any = null; + try { + dataObject = JSON.parse(data); + } + catch (err){ + Utils.showUserMessage("Error parsing file JSON", err.message); + if (callback !== null) callback([ActionMessage.Error(err)], file, file.type, null); + return; + } - case Eagle.FileType.Palette: - this._remotePaletteLoaded(file, data); - break; + const fileTypeLoaded: Eagle.FileType = Utils.determineFileType(dataObject); + // console.log("fileTypeLoaded", fileTypeLoaded); - case Eagle.FileType.Markdown: - Utils.showUserMessage(file.name, Utils.markdown2html(data)); - break; + // attempt to determine schema version from FileInfo + const eagleVersion: string = Utils.determineEagleVersion(dataObject); - default: - // Show error message - Utils.showUserMessage("Error", "The file type is neither graph nor palette!"); + // warn user if file newer than EAGLE + if (Utils.newerEagleVersion(eagleVersion, (window).version)){ + Utils.requestUserConfirm("Newer EAGLE Version", "File " + file.name + " was written with EAGLE version " + eagleVersion + ", whereas the current EAGLE version is " + (window).version + ". Do you wish to load the file anyway?", "Yes", "No", "", (confirmed : boolean) : void => { + if (confirmed){ + this._load(fileTypeLoaded, dataObject, data, file, callback); + } + }); + } else { + this._load(fileTypeLoaded, dataObject, data, file, callback); } - this.resetEditor() }); }; - _loadGraph = (dataObject: any, file: RepositoryFile) : void => { - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; + _load = (fileTypeLoaded: Eagle.FileType, dataObject: any, data: string, file: RepositoryFile, callback: (errors: ActionMessage[], file: RepositoryFile, fileTypeLoaded: Eagle.FileType, object: LogicalGraph | Palette) => void): void => { + const errors: ActionMessage[] = []; + let logicalGraph: LogicalGraph = null; + let palette: Palette = null; - // load graph - this.logicalGraph(LogicalGraph.fromOJSJson(dataObject, file, errorsWarnings)); + switch (fileTypeLoaded){ + case Eagle.FileType.Graph: + logicalGraph = LogicalGraph.fromOJSJson(dataObject, file, errors); + break; - // show errors/warnings - this._handleLoadingErrors(errorsWarnings, file.name, file.repository.service); + case Eagle.FileType.Palette: + palette = Palette.fromOJSJson(dataObject, file, errors); + break; - // center graph - this.centerGraph(); + case Eagle.FileType.Markdown: + Utils.showUserMessage(file.name, Utils.markdown2html(data)); + break; - // check graph - this.checkGraph(); - this.undo().pushSnapshot(this, "Loaded " + file.name); + default: + // Show error message + Utils.showUserMessage("Error", "The file type is unknown!"); + } + this.resetEditor(); - // if the fileType is the same as the current mode, update the activeFileInfo with details of the repository the file was loaded from - this.updateLogicalGraphFileInfo(file.repository.service, file.repository.name, file.repository.branch, file.path, file.name); + if (callback !== null){ + callback(errors, file, fileTypeLoaded, logicalGraph !== null ? logicalGraph : palette); + } } + /* + defaultLoad = (errors: ActionMessage[], file: RepositoryFile, fileTypeLoaded: Eagle.FileType, object: LogicalGraph | Palette) : void => { + if (fileTypeLoaded === Eagle.FileType.Palette){ + this.remotePaletteLoaded(file, object); + } + + // handle graphs + if (fileTypeLoaded === Eagle.FileType.Graph){ + this.remoteGraphLoaded(file, object); + } + + // handle errors + this.handleLoadingErrors([{file: file, errors: errors}]); + } + */ + insertRemoteFile = (file : RepositoryFile) : void => { // flag file as being fetched file.isFetching(true); @@ -1820,7 +1955,7 @@ export class Eagle { } // load file from github or gitlab - insertRemoteFileFunc(file.repository.service, file.repository.name, file.repository.branch, file.path, file.name, (error : string, data : string) : void => { + insertRemoteFileFunc(file, (error : string, data : string) : void => { // flag fetching as complete file.isFetching(false); @@ -1853,14 +1988,14 @@ export class Eagle { // attempt to determine schema version from FileInfo const schemaVersion: Daliuge.SchemaVersion = Utils.determineSchemaVersion(dataObject); - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; + const errors: ActionMessage[] = []; // use the correct parsing function based on schema version let lg: LogicalGraph; switch (schemaVersion){ case Daliuge.SchemaVersion.OJS: case Daliuge.SchemaVersion.Unknown: - lg = LogicalGraph.fromOJSJson(dataObject, file, errorsWarnings); + lg = LogicalGraph.fromOJSJson(dataObject, file, errors); break; } @@ -1868,21 +2003,19 @@ export class Eagle { const parentNode: Node = new Node(Utils.newKey(this.logicalGraph().getNodes()), lg.fileInfo().name, lg.fileInfo().getText(), Category.SubGraph); // perform insert - this.insertGraph(lg.getNodes(), lg.getEdges(), parentNode, errorsWarnings); + this.insertGraph(lg.getNodes(), lg.getEdges(), parentNode, errors); // trigger re-render this.logicalGraph.valueHasMutated(); this.undo().pushSnapshot(this, "Inserted " + file.name); - this.checkGraph(); + this.graphChecker().check(); // show errors/warnings - this._handleLoadingErrors(errorsWarnings, file.name, file.repository.service); + this.handleLoadingErrors([{file: file, errors:errors}]); }); }; - private _remotePaletteLoaded = (file : RepositoryFile, data : string) : void => { - // load the remote palette into EAGLE's palettes object. - + public remotePaletteLoaded = (file : RepositoryFile, palette: Palette) : void => { // check palette is not already loaded const alreadyLoadedPalette : Palette = this.findPaletteByFile(file); @@ -1890,49 +2023,59 @@ export class Eagle { if (alreadyLoadedPalette !== null && Setting.findValue(Setting.CONFIRM_RELOAD_PALETTES)){ Utils.requestUserConfirm("Reload Palette?", "This palette (" + file.name + ") is already loaded, do you wish to load it again?", "Yes", "No",Setting.CONFIRM_RELOAD_PALETTES, (confirmed : boolean) : void => { if (confirmed){ - this._reloadPalette(file, data, alreadyLoadedPalette); + this._reloadPalette(file, palette, alreadyLoadedPalette); } }); } else { - this._reloadPalette(file, data, alreadyLoadedPalette); + this._reloadPalette(file, palette, alreadyLoadedPalette); } } - private _reloadPalette = (file : RepositoryFile, data : string, palette : Palette) : void => { + public remoteGraphLoaded = (file: RepositoryFile, logicalGraph: LogicalGraph) : void => { + // update graph + this.logicalGraph(logicalGraph); + + // center graph + this.centerGraph(); + + // check graph + this.graphChecker().check(); + this.undo().pushSnapshot(this, "Loaded " + file.name); + + // if the fileType is the same as the current mode, update the activeFileInfo with details of the repository the file was loaded from + this.updateLogicalGraphFileInfo(file); + } + + private _reloadPalette = (file : RepositoryFile, newPalette : Palette, oldPalette : Palette) : void => { // close the existing version of the open palette - if (palette !== null){ - this.closePalette(palette); + if (oldPalette !== null){ + this.closePalette(oldPalette); } - // load the new palette - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; - const newPalette = Palette.fromOJSJson(data, file, errorsWarnings); - // sort items in palette newPalette.sort(); // add to list of palettes - this.palettes.unshift(newPalette); + this.palettes.push(newPalette); // show errors/warnings - this._handleLoadingErrors(errorsWarnings, file.name, file.repository.service); + //this.handleLoadingErrors(errorsWarnings, file.name, file.repository.service); this.leftWindow().shown(true); } - private updateLogicalGraphFileInfo = (repositoryService : Eagle.RepositoryService, repositoryName : string, repositoryBranch : string, path : string, name : string) : void => { - // console.log("updateLogicalGraphFileInfo(): repositoryService:", repositoryService, "repositoryName:", repositoryName, "repositoryBranch:", repositoryBranch, "path:", path, "name:", name); + updateLogicalGraphFileInfo = (file: RepositoryFile) : void => { + //console.log("updateLogicalGraphFileInfo(): repositoryService:", file.repository.service, "repositoryName:", file.repository.name, "repositoryBranch:", file.repository.branch, "path:", file.path, "name:", file.name); // update the activeFileInfo with details of the repository the file was loaded from - this.logicalGraph().fileInfo().repositoryName = repositoryName; - this.logicalGraph().fileInfo().repositoryBranch = repositoryBranch; - this.logicalGraph().fileInfo().repositoryService = repositoryService; - this.logicalGraph().fileInfo().path = path; - this.logicalGraph().fileInfo().name = name; + this.logicalGraph().fileInfo().repositoryName = file.repository.name; + this.logicalGraph().fileInfo().repositoryBranch = file.repository.branch; + this.logicalGraph().fileInfo().repositoryService = file.repository.service; + this.logicalGraph().fileInfo().path = file.path; + this.logicalGraph().fileInfo().name = file.name; // communicate to knockout that the value of the fileInfo has been modified (so it can update UI) this.logicalGraph().fileInfo.valueHasMutated(); - } findPaletteByFile = (file : RepositoryFile) : Palette => { @@ -1995,7 +2138,7 @@ export class Eagle { Setting.setValue(Setting.CONFIRM_DISCARD_CHANGES,true) Setting.setValue(Setting.CONFIRM_NODE_CATEGORY_CHANGES,true) Setting.setValue(Setting.CONFIRM_RELOAD_PALETTES,true) - Setting.setValue(Setting.CONFIRM_REMOVE_REPOSITORES,true) + Setting.setValue(Setting.CONFIRM_REMOVE_REPOSITORIES,true) Utils.showNotification("Success", "Confirmation message pop ups re-enabled", "success"); } @@ -2017,16 +2160,7 @@ export class Eagle { const jsonString: string = Palette.toOJSJsonString(p_clone); // validate json - if (!Setting.findValue(Setting.DISABLE_JSON_VALIDATION)){ - const jsonObject = JSON.parse(jsonString); - const validatorResult : {valid: boolean, errors: string} = Utils.validateJSON(jsonObject, Daliuge.SchemaVersion.OJS, Eagle.FileType.Palette); - if (!validatorResult.valid){ - const message = "JSON Output failed validation against internal JSON schema, saving anyway"; - console.error(message, validatorResult.errors); - Utils.showUserMessage("Error", message + "
" + validatorResult.errors); - //return; - } - } + Utils.validateJSON(jsonString, Eagle.FileType.Palette); Utils.httpPostJSONString('/saveFileToLocal', jsonString, (error : string, data : string) : void => { if (error != null){ @@ -2061,12 +2195,11 @@ export class Eagle { return; } - let fileName = graph.fileInfo().name; - // generate filename if necessary - if (fileName === "") { - fileName = "Diagram-" + Utils.generateDateTimeString() + "." + Utils.getDiagramExtension(Eagle.FileType.Graph); - graph.fileInfo().name = fileName; + if (graph.fileInfo().name === "") { + // abort and notify user + Utils.showNotification("Unable to save Graph with no name", "Please name the graph before saving", "danger"); + return; } // clone the logical graph and remove github info ready for local save @@ -2076,16 +2209,7 @@ export class Eagle { const jsonString : string = LogicalGraph.toOJSJsonString(lg_clone, false); // validate json - if (!Setting.findValue(Setting.DISABLE_JSON_VALIDATION)){ - const jsonObject = JSON.parse(jsonString); - const validatorResult : {valid: boolean, errors: string} = Utils.validateJSON(jsonObject, Daliuge.SchemaVersion.OJS, Eagle.FileType.Graph); - if (!validatorResult.valid){ - const message = "JSON Output failed validation against internal JSON schema, saving anyway"; - console.error(message, validatorResult.errors); - Utils.showUserMessage("Error", message + "
" + validatorResult.errors); - //return; - } - } + Utils.validateJSON(jsonString, Eagle.FileType.Graph); Utils.httpPostJSONString('/saveFileToLocal', jsonString, (error : string, data : string) : void => { if (error != null){ @@ -2094,7 +2218,7 @@ export class Eagle { return; } - Utils.downloadFile(error, data, fileName); + Utils.downloadFile(error, data, graph.fileInfo().name); // since changes are now stored locally, the file will have become out of sync with the GitHub repository, so the association should be broken // clear the modified flag @@ -2186,7 +2310,7 @@ export class Eagle { } saveAsPNG = () : void => { - Utils.saveAsPNG('#logicalGraphD3Div svg', this.logicalGraph().fileInfo().name); + Utils.saveAsPNG('#logicalGraph svg', this.logicalGraph().fileInfo().name); }; toggleCollapseAllGroups = () : void => { @@ -2255,7 +2379,7 @@ export class Eagle { sourceNode.setGroupEnd(this.selectedEdge().isClosesLoop()); destNode.setGroupStart(this.selectedEdge().isClosesLoop()); - this.checkGraph(); + this.graphChecker().check(); const groupStartValue = destNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_START).getValue(); const groupEndValue = sourceNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_END).getValue(); @@ -2318,8 +2442,17 @@ export class Eagle { Utils.hideSettingsModal(); } - closeErrorsModal = () : void => { - Utils.closeErrorsModal(); + openCheckGraphModal = (): void => { + if (this.graphChecker().issues().length > 0){ + // show graph modal + this.smartToggleModal('checkGraphModal') + } else { + Utils.showNotification("Check Graph", "Graph OK", "success"); + } + } + + closeCheckGraphModal = () : void => { + Utils.closeCheckGraphModal(); } smartToggleModal = (modal:string) : void => { @@ -2352,7 +2485,9 @@ export class Eagle { this.showTableModal(true) if(selectType === 'rightClick'){ this.setSelection(Eagle.RightWindowMode.Inspector, Eagle.selectedRightClickObject(), Eagle.selectedRightClickLocation()) - $('#customContextMenu').remove(); + + RightClick.closeCustomContextMenu(true); + setTimeout(function() { Utils.showOpenParamsTableModal(mode); }, 30); @@ -2483,9 +2618,11 @@ export class Eagle { return; } + const eagle: Eagle = Eagle.getInstance(); + // validate edge - const isValid: Eagle.LinkValid = Edge.isValid(this, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); - if (isValid === Eagle.LinkValid.Invalid || isValid === Eagle.LinkValid.Unknown){ + const isValid: Eagle.LinkValid = Edge.isValid(eagle.logicalGraph(), edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); + if (isValid === Eagle.LinkValid.Impossible || isValid === Eagle.LinkValid.Invalid || isValid === Eagle.LinkValid.Unknown){ Utils.showUserMessage("Error", "Invalid edge"); return; } @@ -2497,7 +2634,7 @@ export class Eagle { // new edges might require creation of new nodes, don't use addEdgeComplete() here! this.addEdge(srcNode, srcPort, destNode, destPort, edge.isLoopAware(), edge.isClosesLoop(), () => { - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Add edge"); // trigger the diagram to re-draw with the modified edge this.logicalGraph.valueHasMutated(); @@ -2507,6 +2644,7 @@ export class Eagle { editSelectedEdge = () : void => { const selectedEdge: Edge = this.selectedEdge(); + const eagle: Eagle = Eagle.getInstance(); if (selectedEdge === null){ console.log("Unable to edit selected edge: No edge selected"); @@ -2529,8 +2667,8 @@ export class Eagle { } // validate edge - const isValid: Eagle.LinkValid = Edge.isValid(this, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); - if (isValid === Eagle.LinkValid.Invalid || isValid === Eagle.LinkValid.Unknown){ + const isValid: Eagle.LinkValid = Edge.isValid(eagle.logicalGraph(), edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); + if (isValid === Eagle.LinkValid.Impossible || isValid === Eagle.LinkValid.Invalid || isValid === Eagle.LinkValid.Unknown){ Utils.showUserMessage("Error", "Invalid edge"); return; } @@ -2543,7 +2681,7 @@ export class Eagle { // new edges might require creation of new nodes, we delete the existing edge and then create a new one using the full new edge pathway this.logicalGraph().removeEdgeById(selectedEdge.getId()); this.addEdge(srcNode, srcPort, destNode, destPort, edge.isLoopAware(), edge.isClosesLoop(), () => { - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Edit edge"); // trigger the diagram to re-draw with the modified edge this.logicalGraph.valueHasMutated(); @@ -2576,7 +2714,7 @@ export class Eagle { const nodes : Node[] = []; const edges : Edge[] = []; - const errorsWarnings : Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; + const errors : ActionMessage[] = []; // split objects into nodes and edges for (const object of incomingNodes){ @@ -2589,8 +2727,8 @@ export class Eagle { } } - this.insertGraph(nodes, edges, null, errorsWarnings); - this.checkGraph(); + this.insertGraph(nodes, edges, null, errors); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Duplicate selection"); this.logicalGraph.valueHasMutated(); } @@ -2741,12 +2879,12 @@ export class Eagle { return; } - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; + const errors : ActionMessage[] = []; const nodes : Node[] = []; const edges : Edge[] = []; for (const n of clipboard.nodes){ - const node = Node.fromOJSJson(n, errorsWarnings, false, (): number => { + const node = Node.fromOJSJson(n, errors, false, (): number => { console.error("Should not have to generate new key for node", n); return 0; }); @@ -2755,22 +2893,22 @@ export class Eagle { } for (const e of clipboard.edges){ - const edge = Edge.fromOJSJson(e, errorsWarnings); + const edge = Edge.fromOJSJson(e, errors); edges.push(edge); } - this.insertGraph(nodes, edges, null, errorsWarnings); + this.insertGraph(nodes, edges, null, errors); // display notification to user - if (Errors.hasErrors(errorsWarnings) || Errors.hasWarnings(errorsWarnings)){ + if (ActionList.hasErrors(errors) || ActionList.hasWarnings(errors)){ } else { Utils.showNotification("Pasted from clipboard", "Pasted " + clipboard.nodes.length + " nodes and " + clipboard.edges.length + " edges.", "info"); } // ensure changes are reflected in display - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Paste from Clipboard"); this.logicalGraph.valueHasMutated(); } @@ -2889,12 +3027,12 @@ export class Eagle { } // TODO: requestMode param here is spelt incorrectly, should be an enum? - deleteSelection = (requstMode:any, suppressUserConfirmationRequest: boolean, deleteChildren: boolean) : void => { + deleteSelection = (requestMode:any, suppressUserConfirmationRequest: boolean, deleteChildren: boolean) : void => { let mode: string; // TODO: should be an enum? let data: any = []; // TODO: declare type // if no objects selected, warn user - if (requstMode === ''){ + if (requestMode === ''){ data = this.selectedObjects() mode = 'normal' }else{ @@ -3041,7 +3179,7 @@ export class Eagle { // flag LG has changed this.logicalGraph().fileInfo().modified = true; - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Delete Selection"); } @@ -3080,16 +3218,16 @@ export class Eagle { } } - addNodeToLogicalGraphAndConnect = (newNode:Node) : void => { - this.addNodeToLogicalGraph(newNode,(node: Node)=>{ - const realSourceNode = RightClick.edgeDropSrcNode; - const realSourcePort = RightClick.edgeDropSrcPort; - const realDestNode = node; + addNodeToLogicalGraphAndConnect = (newNodeId: string) : void => { + this.addNodeToLogicalGraph(null, newNodeId, Eagle.AddNodeMode.ContextMenu, (node: Node)=>{ + const realSourceNode: Node = RightClick.edgeDropSrcNode; + const realSourcePort: Field = RightClick.edgeDropSrcPort; + const realDestNode: Node = node; let realDestPort = node.findPortByMatchingType(realSourcePort.getType(), !RightClick.edgeDropSrcIsInput); // if no dest port was found, just use first input port on dest node if (realDestPort === null){ - realDestPort = node.findPortOfAnyType(realSourcePort.getType()); + realDestPort = node.findPortOfAnyType(true); } // create edge (in correct direction) @@ -3098,10 +3236,10 @@ export class Eagle { } else { this.addEdge(realDestNode, realDestPort, realSourceNode, realSourcePort, false, false, null); } - },'contextMenu') + }); } - addNodeToLogicalGraph = (node : any, callback: (node: Node) => void, mode:string) : void => { + addNodeToLogicalGraph = (node: Node, nodeId: string, mode: Eagle.AddNodeMode, callback: (node: Node) => void) : void => { let pos : {x:number, y:number}; pos = {x:0,y:0} @@ -3112,34 +3250,34 @@ export class Eagle { return; } - if(mode === 'contextMenu'){ + if(mode === Eagle.AddNodeMode.ContextMenu){ let nodeFound = false pos = Eagle.selectedRightClickPosition; this.palettes().forEach(function(palette){ - if(palette.findNodeById(node)!==null){ - node = palette.findNodeById(node) + if(palette.findNodeById(nodeId)!==null){ + node = palette.findNodeById(nodeId) nodeFound = true } }) if (!nodeFound){ - node = this.logicalGraph().findNodeById(node) + node = this.logicalGraph().findNodeById(nodeId) } - $('#customContextMenu').remove() + + RightClick.closeCustomContextMenu(true); } // if node is a construct, set width and height a little larger - if (CategoryData.getCategoryData(node.getCategory()).canContainComponents){ - node.setWidth(Node.GROUP_DEFAULT_WIDTH); - node.setHeight(Node.GROUP_DEFAULT_HEIGHT); + if (node.isGroup()){ + node.setRadius(GraphConfig.MINIMUM_CONSTRUCT_RADIUS); } //if pos is 0 0 then we are not using drop location nor right click location. so we try to determine a logical place to put it if(pos.x === 0 && pos.y === 0){ // get new position for node if (Eagle.nodeDropLocation.x === 0 && Eagle.nodeDropLocation.y === 0){ - pos = this.getNewNodePosition(node.getWidth(), node.getHeight()); + pos = this.getNewNodePosition(node.getRadius(), node.getRadius()); } else { pos = Eagle.nodeDropLocation; } @@ -3153,7 +3291,7 @@ export class Eagle { newNode.setCollapsed(false); // set parent (if the node was dropped on something) - const parent : Node = this.logicalGraph().checkForNodeAt(newNode.getPosition().x, newNode.getPosition().y, newNode.getWidth(), newNode.getHeight(), newNode.getKey(), true); + const parent : Node = this.logicalGraph().checkForNodeAt(newNode.getPosition().x, newNode.getPosition().y, newNode.getRadius(), newNode.getKey(), true); // if a parent was found, update if (parent !== null && newNode.getParentKey() !== parent.getKey() && newNode.getKey() !== parent.getKey()){ @@ -3173,7 +3311,7 @@ export class Eagle { const poNode: Node = new Node(Utils.newKey(this.logicalGraph().getNodes()), "Object", "Instance of Object", Category.PythonObject); // add node to LogicalGraph - const OBJECT_OFFSET_X = 300; + const OBJECT_OFFSET_X = 0; const OBJECT_OFFSET_Y = 0; this.addNode(poNode, pos.x + OBJECT_OFFSET_X, pos.y + OBJECT_OFFSET_Y, (pythonObjectNode: Node) => { // set parent to same as PythonMemberFunction @@ -3205,7 +3343,7 @@ export class Eagle { }); } - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Add node " + newNode.getName()); this.logicalGraph.valueHasMutated(); @@ -3532,8 +3670,9 @@ export class Eagle { //handling selecting and highlighting the newly created row const clickTarget = $($("#paramsTableWrapper tbody").children()[fieldIndex]).find('.selectionTargets')[0] - clickTarget.click() //simply clicking the element is best as it also lets knockout handle all of the selection and obsrevable update processes + clickTarget.click() //simply clicking the element is best as it also lets knockout handle all of the selection and observable update processes clickTarget.focus() // used to focus the field allowing the user to immediately start typing + $(clickTarget).select() //scroll to new row $("#parameterTableModal .modal-body").animate({ @@ -3638,7 +3777,7 @@ export class Eagle { continue; } - //this index only counts up if the above doesnt filter out the choice + // this index only counts up if the above doesn't filter out the choice validChoiceIndex++ // if this node is already the parent, note its index, so that we can preselect this parent node in the modal dialog @@ -3670,7 +3809,7 @@ export class Eagle { } // refresh the display - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Change Node Parent"); this.selectedObjects.valueHasMutated(); this.logicalGraph.valueHasMutated(); @@ -3718,7 +3857,7 @@ export class Eagle { selectedNode.setSubjectKey(newSubjectKey); // refresh the display - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Change Node Subject"); this.selectedObjects.valueHasMutated(); this.logicalGraph.valueHasMutated(); @@ -3764,7 +3903,7 @@ export class Eagle { destinationPort.setType(newType); // flag changes - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Change Edge Data Type"); this.selectedObjects.valueHasMutated(); this.logicalGraph.valueHasMutated(); @@ -3792,7 +3931,7 @@ export class Eagle { } } - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Remove port from node"); this.flagActiveFileModified(); this.selectedObjects.valueHasMutated(); @@ -3800,7 +3939,7 @@ export class Eagle { nodeDropLogicalGraph = (eagle : Eagle, e : JQueryEventObject) : void => { // keep track of the drop location - Eagle.nodeDropLocation = this.getNodeDropLocation(e); + Eagle.nodeDropLocation = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(e.pageX),y:GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(e.pageY)} // determine dropped node const sourceComponents : Node[] = []; @@ -3826,7 +3965,7 @@ export class Eagle { // add each of the nodes we are moving for (const sourceComponent of sourceComponents){ - this.addNodeToLogicalGraph(sourceComponent, null,''); + this.addNodeToLogicalGraph(sourceComponent, "", Eagle.AddNodeMode.Default, null); // to avoid placing all the selected nodes on top of each other at the same spot, we increment the nodeDropLocation after each node Eagle.nodeDropLocation.x += 20; @@ -3899,8 +4038,8 @@ export class Eagle { y = y - offset.top; // transform display coords into real coords - x = (x - this.globalOffsetX)/this.globalScale; - y = (y - this.globalOffsetY)/this.globalScale; + x = (x - this.globalOffsetX())/this.globalScale(); + y = (y - this.globalOffsetY())/this.globalScale(); return {x:x, y:y}; }; @@ -3992,7 +4131,7 @@ export class Eagle { node.addField(clone); } - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Add field"); }); @@ -4019,7 +4158,7 @@ export class Eagle { // update field data (keep existing nodeKey and id) field.copyWithKeyAndId(newField, field.getNodeKey(), field.getId()); - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Edit Field"); // if we summoned this editField modal from the params table, now that we are done, re-open the params table @@ -4049,8 +4188,9 @@ export class Eagle { setTimeout(function() { //handling selecting and highlighting the newly created node const clickTarget = $($("#paramsTableWrapper tbody").children()[fieldIndex]).find('.selectionTargets')[0] - clickTarget.click() //simply clicking the element is best as it also lets knockout handle all of the selection and obsrevable update process + clickTarget.click() //simply clicking the element is best as it also lets knockout handle all of the selection and observable update process clickTarget.focus() //used to focus the field allowing the user to immediately start typing + $(clickTarget).select() $("#parameterTableModal .modal-body").animate({ scrollTop: (fieldIndex*30) @@ -4138,8 +4278,6 @@ export class Eagle { // clone the input application to make a local copy // TODO: at the moment, this clone just 'exists' nowhere in particular, but it should be added to the components dict in JSON V3 const clone : Node = application.clone(); - const newKey : number = Utils.newKey(this.logicalGraph().getNodes()); - clone.setKey(newKey); callback(clone); }); @@ -4154,15 +4292,27 @@ export class Eagle { this.setNodeApplication("Input Application", "Choose an input application", (inputApplication: Node) => { const node: Node = this.logicalGraph().findNodeByKey(nodeKey); const oldApp: Node = node.getInputApplication(); + const clone : Node = inputApplication.clone(); // remove all edges incident on the old input application if (oldApp !== null){ this.logicalGraph().removeEdgesByKey(oldApp.getKey()); } - node.setInputApplication(inputApplication); + if(clone.getFields() != null){ + // set new ids for any fields in this node + for (const field of clone.getFields()){ + field.setId(Utils.uuidv4()); + } + } + + node.setInputApplication(clone); - this.checkGraph(); + node.getInputApplication().setKey(Utils.newKey(this.logicalGraph().getNodes())); + node.getInputApplication().setId(Utils.uuidv4()); + node.getInputApplication().setEmbedKey(node.getKey()); + + this.graphChecker().check(); this.undo().pushSnapshot(this, "Set Node Input Application"); }); } @@ -4176,34 +4326,45 @@ export class Eagle { this.setNodeApplication("Output Application", "Choose an output application", (outputApplication: Node) => { const node: Node = this.logicalGraph().findNodeByKey(nodeKey); const oldApp: Node = node.getOutputApplication(); + const clone : Node = outputApplication.clone(); // remove all edges incident on the old output application if (oldApp !== null){ this.logicalGraph().removeEdgesByKey(oldApp.getKey()); } + if(clone.getFields() != null){ + // set new ids for any fields in this node + for (const field of clone.getFields()){ + field.setId(Utils.uuidv4()); + } + } - node.setOutputApplication(outputApplication); + node.setOutputApplication(clone); - this.checkGraph(); + node.getOutputApplication().setKey(Utils.newKey(this.logicalGraph().getNodes())); + node.getOutputApplication().setId(Utils.uuidv4()); + node.getOutputApplication().setEmbedKey(node.getKey()); + + this.graphChecker().check(); this.undo().pushSnapshot(this, "Set Node Output Application"); }); } - getNewNodePosition = (width:number, height:number) : {x:number, y:number} => { + getNewNodePosition = (width: number, height: number) : {x:number, y:number} => { const MARGIN = 100; // buffer to keep new nodes away from the maxX and maxY sides of the LG display area + const navBarHeight = 84 let suitablePositionFound = false; let numIterations = 0; const MAX_ITERATIONS = 100; let x; let y; - + while (!suitablePositionFound && numIterations <= MAX_ITERATIONS){ // get visible screen size - const minX = this.leftWindow().shown() ? this.leftWindow().width(): 0; - const maxX = this.rightWindow().shown() ? $('#logicalGraphD3Div').width() - this.rightWindow().width() - width - MARGIN : $('#logicalGraphD3Div').width() - width - MARGIN; - const minY = 0; - const maxY = $('#logicalGraphD3Div').height() - height - MARGIN; - + const minX = this.leftWindow().shown() ? this.leftWindow().width()+MARGIN: 0+MARGIN; + const maxX = this.rightWindow().shown() ? $('#logicalGraphParent').width() - this.rightWindow().width() - width - MARGIN : $('#logicalGraphParent').width() - width - MARGIN; + const minY = 0 + navBarHeight + MARGIN; + const maxY = $('#logicalGraphParent').height() - height - MARGIN + navBarHeight; // choose random position within minimums and maximums determined above const randomX = Math.floor(Math.random() * (maxX - minX + 1) + minX); const randomY = Math.floor(Math.random() * (maxY - minY + 1) + minY); @@ -4211,14 +4372,9 @@ export class Eagle { x = randomX; y = randomY; - // modify random positions using current translation of viewport - x -= this.globalOffsetX; - y -= this.globalOffsetY; - - x /= this.globalScale; - y /= this.globalScale; - - //console.log("Candidate Position", numIterations, ":", x, ",", y, "X:", minX, "-", maxX, "Y:", minY, "-", maxY); + //translate the chosen arandomised position into graph co-ordinates + x= GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(x) + y=GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(y) // check position is suitable, doesn't collide with any existing nodes const collision = this.logicalGraph().checkForNodeAt(x, y, width, height, null); @@ -4255,33 +4411,13 @@ export class Eagle { graph_url += "&path=" + encodeURI(fileInfo.path); graph_url += "&filename=" + encodeURI(fileInfo.name); - // copy to cliboard + // copy to clipboard navigator.clipboard.writeText(graph_url); // notification Utils.showNotification("Graph URL", "Copied to clipboard", "success"); } - checkGraph = (): void => { - const checkResult = Utils.checkGraph(this); - - this.graphWarnings(checkResult.warnings); - this.graphErrors(checkResult.errors); - }; - - showGraphErrors = (): void => { - if (this.graphWarnings().length > 0 || this.graphErrors().length > 0){ - - // switch to graph errors mode - this.errorsMode(Setting.ErrorsMode.Graph); - - // show graph modal - this.smartToggleModal('errorsModal') - } else { - Utils.showNotification("Check Graph", "Graph OK", "success"); - } - } - addEdge = (srcNode: Node, srcPort: Field, destNode: Node, destPort: Field, loopAware: boolean, closesLoop: boolean, callback: (edge: Edge) => void) : void => { // check that none of the supplied nodes and ports are null if (srcNode === null){ @@ -4491,7 +4627,7 @@ export class Eagle { } this.flagActiveFileModified(); - this.checkGraph(); + this.graphChecker().check(); this.undo().pushSnapshot(this, "Edit Node Category"); this.logicalGraph.valueHasMutated(); } @@ -4506,7 +4642,6 @@ export class Eagle { newNode.setKey(Utils.newKey(this.logicalGraph().getNodes())); newNode.setPosition(x, y); newNode.setEmbedKey(null); - this.logicalGraph().addNodeComplete(newNode); // set new ids for any fields in this node @@ -4514,9 +4649,22 @@ export class Eagle { field.setId(Utils.uuidv4()); } + // console.log(node.hasInputApplication(),node.getInputApplication(),newNode.hasInputApplication(),newNode.getInputApplication()) // set new keys for embedded applications within node, and new ids for ports within those embedded nodes - if (newNode.hasInputApplication()){ + if (node.hasInputApplication()){ + const clone : Node = node.getInputApplication().clone(); + + if(clone.getFields() != null){ + // set new ids for any fields in this node + for (const field of clone.getFields()){ + field.setId(Utils.uuidv4()); + } + } + newNode.setInputApplication(clone) + // console.log(node.hasInputApplication(),node.getInputApplication(),newNode.hasInputApplication(),newNode.getInputApplication()) + newNode.getInputApplication().setKey(Utils.newKey(this.logicalGraph().getNodes())); + newNode.getInputApplication().setId(Utils.uuidv4()); newNode.getInputApplication().setEmbedKey(newNode.getKey()); // set new ids for any fields in this node @@ -4524,8 +4672,19 @@ export class Eagle { field.setId(Utils.uuidv4()); } } - if (newNode.hasOutputApplication()){ + if (node.hasOutputApplication()){ + const clone : Node = node.getOutputApplication().clone(); + + if(clone.getFields() != null){ + // set new ids for any fields in this node + for (const field of clone.getFields()){ + field.setId(Utils.uuidv4()); + } + } + newNode.setOutputApplication(clone) + newNode.getOutputApplication().setKey(Utils.newKey(this.logicalGraph().getNodes())); + newNode.getOutputApplication().setId(Utils.uuidv4()); newNode.getOutputApplication().setEmbedKey(newNode.getKey()); // set new ids for any fields in this node @@ -4538,31 +4697,31 @@ export class Eagle { this.logicalGraph().fileInfo().modified = true; this.logicalGraph().fileInfo.valueHasMutated(); + // check if node was added to an empty graph, if so prompt user to specify graph name + if (this.logicalGraph().fileInfo().name === ""){ + console.log("Node added to Graph with no name, requesting name from user"); + + this.newDiagram(Eagle.FileType.Graph, (name: string) => { + this.logicalGraph().fileInfo().name = name; + this.graphChecker().check(); + this.undo().pushSnapshot(this, "Named Logical Graph"); + this.logicalGraph.valueHasMutated(); + Utils.showNotification("Graph named", name, "success"); + }); + } + if (callback !== null) callback(newNode); } checkForComponentUpdates = () : void => { console.log("checkForComponentUpdates()"); - ComponentUpdater.update(this.palettes(), this.logicalGraph(), function(errorsWarnings:Errors.ErrorsWarnings, updatedNodes:Node[]){ - console.log("callback", errorsWarnings, updatedNodes); + ComponentUpdater.determineUpdates(this.palettes(), this.logicalGraph(), function(errors: ActionMessage[], updates: ActionMessage[]){ + console.log("callback", errors, updates); + const combined: ActionMessage[] = errors.concat(updates); + console.log("combined", combined); - // report missing palettes to the user - if (errorsWarnings.errors.length > 0){ - const errorStrings = []; - for (const error of errorsWarnings.errors){ - errorStrings.push(error.message); - } - - Utils.showNotification("Error", errorStrings.join("\n"), "danger"); - } else { - const nodeNames = []; - for (const node of updatedNodes){ - nodeNames.push(node.getName()); - } - - Utils.showNotification("Success", "Successfully updated " + updatedNodes.length + " component(s): " + nodeNames.join(", "), "success"); - } + Utils.showActionListModal("Update Graph Components", ActionList.Mode.UpdateComponents, [{source:"", messages:combined}]); }); } } @@ -4582,6 +4741,11 @@ export namespace Eagle Hierarchy = "Hierarchy" } + export enum AddNodeMode { + ContextMenu = "ContextMenu", + Default = "Default" + } + export enum FileType { Graph = "Graph", Palette = "Palette", @@ -4591,10 +4755,11 @@ export namespace Eagle } export enum LinkValid { - Unknown = "Unknown", - Invalid = "Invalid", - Warning = "Warning", - Valid = "Valid" + Unknown = "Unknown", // validity of the edge is unknown + Impossible = "Impossible", // never useful or valid + Invalid = "Invalid", // invalid, but possibly useful for expert users? + Warning = "Warning", // valid, but some issue that the user should be aware of + Valid = "Valid" // fine } export enum ModalType { @@ -4631,15 +4796,15 @@ $( document ).ready(function() { $('.modal').on('hidden.bs.modal', function () { $('.modal-dialog').css({"left":"0px", "top":"0px"}) $("#editFieldModal textarea").attr('style','') - $("#errorsModalAccordion").parent().parent().attr('style','') + $("#checkGraphModalAccordion").parent().parent().attr('style','') - //reset parameter table selecction + //reset parameter table selection ParameterTable.resetSelection() }); $('.modal').on('shown.bs.modal',function(){ // modal draggables - //the any type is required so we dont have an error when building. at runtime on eagle this actually functions without it. + // the any type is required so we don't have an error when building. at runtime on eagle this actually functions without it. ($('.modal-dialog')).draggable({ handle: ".modal-header" }); @@ -4682,7 +4847,7 @@ $( document ).ready(function() { $("textarea").blur(); //back up method of hiding the right click context menu in case it get stuck open - $('#customContextMenu').remove(); + RightClick.closeCustomContextMenu(true); }); $(".tableParameter").on("click", function(){ diff --git a/src/Edge.ts b/src/Edge.ts index a1670cc9e..1dc409570 100644 --- a/src/Edge.ts +++ b/src/Edge.ts @@ -22,15 +22,16 @@ # */ +import { ActionList } from './ActionList'; +import { ActionMessage } from './Action'; import { Category } from './Category'; -import { CategoryData } from './CategoryData'; import { Daliuge } from './Daliuge'; import { Eagle } from './Eagle'; +import { Field } from './Field'; import { LogicalGraph } from './LogicalGraph'; import { Node } from './Node'; -import { Field } from './Field'; import { Utils } from './Utils'; -import { Errors } from './Errors'; +import * as ko from "knockout"; export class Edge { private _id : string @@ -42,6 +43,7 @@ export class Edge { private loopAware : boolean; // indicates the user is aware that the components at either end of the edge may differ in multiplicity private closesLoop : boolean; // indicates that this is a special type of edge that can be drawn in eagle to specify the start/end of groups. private selectionRelative : boolean // indicates if the edge is either selected or attached to a selected node + private isShortEdge : ko.Observable; constructor(srcNodeKey : number, srcPortId : string, destNodeKey : number, destPortId : string, dataType : string, loopAware: boolean, closesLoop: boolean, selectionRelative : boolean){ this._id = Utils.uuidv4(); @@ -55,6 +57,7 @@ export class Edge { this.loopAware = loopAware; this.closesLoop = closesLoop; this.selectionRelative = selectionRelative; + this.isShortEdge = ko.observable(false) } getId = () : string => { @@ -141,6 +144,22 @@ export class Edge { this.selectionRelative = !this.selectionRelative; } + setIsShortEdge = (value:boolean) : void => { + this.isShortEdge(value) + } + + getIsShortEdge = () : boolean => { + return this.isShortEdge() + } + + getArrowVisibility = () : string => { + if (this.isShortEdge()){ + return 'hidden' + }else{ + return 'visible' + } + } + clear = () : void => { this._id = ""; this.srcNodeKey = 0; @@ -160,10 +179,12 @@ export class Edge { return result; } - getErrorsWarnings = (eagle: Eagle): Errors.ErrorsWarnings => { - const result: {warnings: Errors.Issue[], errors: Errors.Issue[]} = {warnings: [], errors: []}; + getErrorsWarnings = (): ActionMessage[] => { + const eagle: Eagle = Eagle.getInstance(); + const result: ActionMessage[] = []; - Edge.isValid(eagle, this._id, this.srcNodeKey, this.srcPortId, this.destNodeKey, this.destPortId, this.dataType, this.loopAware, this.closesLoop, false, false, result); + // TODO: it's a shame we need to pass logicalGraph here + Edge.isValid(eagle.logicalGraph(), this._id, this.srcNodeKey, this.srcPortId, this.destNodeKey, this.destPortId, this.dataType, this.loopAware, this.closesLoop, false, false, result); return result; } @@ -180,7 +201,7 @@ export class Edge { }; } - static fromOJSJson = (linkData: any, errorsWarnings: Errors.ErrorsWarnings) : Edge => { + static fromOJSJson = (linkData: any, errors: ActionMessage[]) : Edge => { // try to read source and destination nodes and ports let srcNodeKey : number = 0; let srcPortId : string = ""; @@ -188,22 +209,22 @@ export class Edge { let destPortId : string = ""; if (typeof linkData.from === 'undefined'){ - errorsWarnings.warnings.push(Errors.Message("Edge is missing a 'from' attribute")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Edge is missing a 'from' attribute")); } else { srcNodeKey = linkData.from; } if (typeof linkData.fromPort === 'undefined'){ - errorsWarnings.warnings.push(Errors.Message("Edge is missing a 'fromPort' attribute")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Edge is missing a 'fromPort' attribute")); } else { srcPortId = linkData.fromPort; } if (typeof linkData.to === 'undefined'){ - errorsWarnings.warnings.push(Errors.Message("Edge is missing a 'to' attribute")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Edge is missing a 'to' attribute")); } else { destNodeKey = linkData.to; } if (typeof linkData.toPort === 'undefined'){ - errorsWarnings.warnings.push(Errors.Message("Edge is missing a 'toPort' attribute")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Edge is missing a 'toPort' attribute")); } else { destPortId = linkData.toPort; } @@ -243,7 +264,7 @@ export class Edge { } } - static fromV3Json = (edgeData: any, errorsWarnings: Errors.ErrorsWarnings): Edge => { + static fromV3Json = (edgeData: any, errors: ActionMessage[]): Edge => { return new Edge(edgeData.srcNode, edgeData.srcPort, edgeData.destNode, edgeData.destPort, "", edgeData.loop_aware === "1", edgeData.closesLoop, false); } @@ -273,11 +294,11 @@ export class Edge { return result; } - static fromAppRefJson = (edgeData: any, errorsWarnings: Errors.ErrorsWarnings): Edge => { + static fromAppRefJson = (edgeData: any, errors: ActionMessage[]): Edge => { return new Edge(edgeData.from, edgeData.fromPort, edgeData.to, edgeData.toPort, edgeData.dataType, edgeData.loopAware, edgeData.closesLoop, false); } - static isValid = (eagle: Eagle, edgeId: string, sourceNodeKey : number, sourcePortId : string, destinationNodeKey : number, destinationPortId : string, dataType: string, loopAware: boolean, closesLoop: boolean, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : Eagle.LinkValid => { + static isValid = (logicalGraph: LogicalGraph, edgeId: string, sourceNodeKey : number, sourcePortId : string, destinationNodeKey : number, destinationPortId : string, dataType: string, loopAware: boolean, closesLoop: boolean, showNotification : boolean, showConsole : boolean, errors: ActionMessage[]) : Eagle.LinkValid => { // check for problems if (isNaN(sourceNodeKey)){ return Eagle.LinkValid.Unknown; @@ -288,32 +309,32 @@ export class Edge { } if (sourcePortId === ""){ - const issue = Errors.Fix("source port has no id", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on source node"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const issue = ActionMessage.Fix(ActionMessage.Level.Error, "source port has no id", function(){Utils.fixNodeFieldIds(sourceNodeKey)}, "Generate ids for ports on source node"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); return Eagle.LinkValid.Invalid; } if (destinationPortId === ""){ - const issue = Errors.Fix("destination port has no id", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on destination node"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const issue = ActionMessage.Fix(ActionMessage.Level.Error, "destination port has no id", function(){Utils.fixNodeFieldIds(sourceNodeKey)}, "Generate ids for ports on destination node"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); return Eagle.LinkValid.Invalid; } if (sourcePortId === null){ - const issue = Errors.Fix("source port id is null", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on source node"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const issue = ActionMessage.Fix(ActionMessage.Level.Error, "source port id is null", function(){Utils.fixNodeFieldIds(sourceNodeKey)}, "Generate ids for ports on source node"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); return Eagle.LinkValid.Invalid; } if (destinationPortId === null){ - const issue = Errors.Fix("destination port id is null", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on destination node"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const issue = ActionMessage.Fix(ActionMessage.Level.Error, "destination port id is null", function(){Utils.fixNodeFieldIds(sourceNodeKey)}, "Generate ids for ports on destination node"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); return Eagle.LinkValid.Invalid; } // get references to actual source and destination nodes (from the keys) - const sourceNode : Node = eagle.logicalGraph().findNodeByKey(sourceNodeKey); - const destinationNode : Node = eagle.logicalGraph().findNodeByKey(destinationNodeKey); + const sourceNode : Node = logicalGraph.findNodeByKey(sourceNodeKey); + const destinationNode : Node = logicalGraph.findNodeByKey(destinationNodeKey); if (sourceNode === null || typeof sourceNode === "undefined" || destinationNode === null || typeof destinationNode === "undefined"){ return Eagle.LinkValid.Unknown; @@ -321,23 +342,24 @@ export class Edge { // check that we are not connecting a Data component to a Data component, that is not supported if (sourceNode.getCategoryType() === Category.Type.Data && destinationNode.getCategoryType() === Category.Type.Data){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Data nodes may not be connected directly to other Data nodes", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, ActionMessage.Show(ActionMessage.Level.Error, "Data nodes may not be connected directly to other Data nodes", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); return Eagle.LinkValid.Invalid; } // if source node or destination node is a construct, then something is wrong, constructs should not have ports if (sourceNode.getCategoryType() === Category.Type.Construct){ - const issue: Errors.Issue = Errors.ShowFix("Edge (" + edgeId + ") cannot have a source node (" + sourceNode.getName() + ") that is a construct", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.fixMoveEdgeToEmbeddedApplication(eagle, edgeId)}, "Move edge to embedded application"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Error, "Edge (" + edgeId + ") cannot have a source node (" + sourceNode.getName() + ") that is a construct", function(){Utils.showEdge(edgeId)}, function(){Utils.fixMoveEdgeToEmbeddedApplication(logicalGraph, edgeId)}, "Move edge to embedded application"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); } if (destinationNode.getCategoryType() === Category.Type.Construct){ - const issue: Errors.Issue = Errors.ShowFix("Edge (" + edgeId + ") cannot have a destination node (" + destinationNode.getName() + ") that is a construct", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.fixMoveEdgeToEmbeddedApplication(eagle, edgeId)}, "Move edge to embedded application"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Error, "Edge (" + edgeId + ") cannot have a destination node (" + destinationNode.getName() + ") that is a construct", function(){Utils.showEdge(edgeId)}, function(){Utils.fixMoveEdgeToEmbeddedApplication(logicalGraph, edgeId)}, "Move edge to embedded application"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); } // check that we are not connecting two ports within the same node if (sourceNodeKey === destinationNodeKey){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("sourceNodeKey and destinationNodeKey are the same", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Impossible, ActionMessage.Show(ActionMessage.Level.Error, "sourceNodeKey and destinationNodeKey are the same", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); + return Eagle.LinkValid.Impossible; } // if source node is a memory, and destination is a BashShellApp, OR @@ -345,8 +367,9 @@ export class Edge { // this is not supported. How would a BashShellApp read data from another process? if ((sourceNode.getCategory() === Category.Memory && destinationNode.getCategory() === Category.BashShellApp) || (sourceNode.getCategory() === Category.Memory && destinationNode.isGroup() && destinationNode.getInputApplication() !== undefined && destinationNode.hasInputApplication() && destinationNode.getInputApplication().getCategory() === Category.BashShellApp)){ - const issue: Errors.Issue = Errors.ShowFix("output from Memory Node cannot be input into a BashShellApp or input into a Group Node with a BashShellApp inputApplicationType", function(){Utils.showNode(eagle, Eagle.FileType.Graph, sourceNode.getId())}, function(){Utils.fixNodeCategory(eagle, sourceNode, Category.File)}, "Change data component type to File"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errorsWarnings); + const eagle: Eagle = Eagle.getInstance(); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Error, "output from Memory Node cannot be input into a BashShellApp or input into a Group Node with a BashShellApp inputApplicationType", function(){Utils.showNode(eagle, Eagle.FileType.Graph, sourceNode.getId())}, function(){Utils.fixNodeCategory(sourceNode, Category.File)}, "Change data component type to File"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, issue, showNotification, showConsole, errors); } const sourcePort : Field = sourceNode.findFieldById(sourcePortId); @@ -354,35 +377,38 @@ export class Edge { // check if source port was found if (sourcePort === null) { - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source port doesn't exist on source node", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); - return Eagle.LinkValid.Invalid; + Edge.isValidLog(edgeId, Eagle.LinkValid.Impossible, ActionMessage.Show(ActionMessage.Level.Error, "Source port doesn't exist on source node", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); + return Eagle.LinkValid.Impossible; } // check if destination port was found if (destinationPort === null){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Destination port doesn't exist on destination node", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); - return Eagle.LinkValid.Invalid; + Edge.isValidLog(edgeId, Eagle.LinkValid.Impossible, ActionMessage.Show(ActionMessage.Level.Error, "Destination port doesn't exist on destination node", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); + return Eagle.LinkValid.Impossible; } // check that we are not connecting a port to itself if (sourcePortId === destinationPortId){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source port and destination port are the same", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Impossible, ActionMessage.Show(ActionMessage.Level.Error, "Source port and destination port are the same", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); + return Eagle.LinkValid.Impossible; } // check that source is output if (!sourcePort.isOutputPort()){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source port is not output port (" + sourcePort.getUsage() + ")", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Impossible, ActionMessage.Show(ActionMessage.Level.Error, "Source port is not output port (" + sourcePort.getUsage() + ")", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); + return Eagle.LinkValid.Impossible; } // check that destination in input if (!destinationPort.isInputPort()){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Destination port is not input port (" + destinationPort.getUsage() + ")", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Impossible, ActionMessage.Show(ActionMessage.Level.Error, "Destination port is not input port (" + destinationPort.getUsage() + ")", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); + return Eagle.LinkValid.Impossible; } if (sourcePort !== null && destinationPort !== null){ // check that source and destination port are both event, or both not event if ((sourcePort.getIsEvent() && !destinationPort.getIsEvent()) || (!sourcePort.getIsEvent() && destinationPort.getIsEvent())){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source port and destination port are mix of event and non-event ports", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, ActionMessage.Show(ActionMessage.Level.Error, "Source port and destination port are mix of event and non-event ports", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); } } @@ -396,13 +422,13 @@ export class Edge { // determine if the new edge is crossing a ExclusiveForceNode boundary if (destinationNode.getParentKey() !== null){ - if (eagle.logicalGraph().findNodeByKey(destinationNode.getParentKey()) !== null){ - parentIsEFN = eagle.logicalGraph().findNodeByKey(destinationNode.getParentKey()).getCategory() === Category.ExclusiveForceNode; + if (logicalGraph.findNodeByKey(destinationNode.getParentKey()) !== null){ + parentIsEFN = logicalGraph.findNodeByKey(destinationNode.getParentKey()).getCategory() === Category.ExclusiveForceNode; } } if (sourceNode.getParentKey() !== null){ - if (eagle.logicalGraph().findNodeByKey(sourceNode.getParentKey()) !== null){ - parentIsEFN = eagle.logicalGraph().findNodeByKey(sourceNode.getParentKey()).getCategory() === Category.ExclusiveForceNode; + if (logicalGraph.findNodeByKey(sourceNode.getParentKey()) !== null){ + parentIsEFN = logicalGraph.findNodeByKey(sourceNode.getParentKey()).getCategory() === Category.ExclusiveForceNode; } } @@ -411,53 +437,57 @@ export class Edge { // if a node is connecting to its parent, it must connect to the local port if (isParent && !destinationNode.hasLocalPortWithId(destinationPortId)){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source port is connecting to its parent, yet destination port is not local", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, ActionMessage.Show(ActionMessage.Level.Error, "Source port is connecting to its parent, yet destination port is not local", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); } // if a node is connecting to a child, it must start from the local port if (isChild && !sourceNode.hasLocalPortWithId(sourcePortId)){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source connecting to child, yet source port is not local", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, ActionMessage.Show(ActionMessage.Level.Error, "Source connecting to child, yet source port is not local", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); } // if destination node is not a child, destination port cannot be a local port if (!parentIsEFN && !isParent && destinationNode.hasLocalPortWithId(destinationPortId)){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Source is not a child of destination, yet destination port is local", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, ActionMessage.Show(ActionMessage.Level.Error, "Source is not a child of destination, yet destination port is local", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); } if (!parentIsEFN && !isChild && sourceNode.hasLocalPortWithId(sourcePortId)){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, Errors.Show("Destination is not a child of source, yet source port is local", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, ActionMessage.Show(ActionMessage.Level.Error, "Destination is not a child of source, yet source port is local", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); } if (sourcePort !== null && destinationPort !== null){ // abort if source port and destination port have different data types if (!Utils.portsMatch(sourcePort, destinationPort)){ - const x = Errors.ShowFix("Source and destination ports don't match: sourcePort (" + sourcePort.getDisplayText() + ":" + sourcePort.getType() + ") destinationPort (" + destinationPort.getDisplayText() + ":" + destinationPort.getType() + ")", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixPortType(eagle, sourcePort, destinationPort);}, "Overwrite destination port type with source port type"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const x = ActionMessage.ShowFix(ActionMessage.Level.Error, "Source and destination ports don't match: sourcePort (" + sourcePort.getDisplayText() + ":" + sourcePort.getType() + ") destinationPort (" + destinationPort.getDisplayText() + ":" + destinationPort.getType() + ")", function(){Utils.showEdge(edgeId);}, function(){Utils.fixPortType(sourcePort, destinationPort);}, "Overwrite destination port type with source port type"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } } // if link is not a parent, child or sibling, then warn user if (!parentIsEFN && !isParent && !isChild && !isSibling && !loopAware && !isParentOfConstruct && !isChildOfConstruct){ - Edge.isValidLog(edgeId, Eagle.LinkValid.Warning, Errors.Show("Edge is not child->parent, parent->child or between siblings. It could be incorrect or computationally expensive", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edgeId, Eagle.LinkValid.Warning, ActionMessage.Show(ActionMessage.Level.Warning, "Edge is not child->parent, parent->child or between siblings. It could be incorrect or computationally expensive", function(){Utils.showEdge(edgeId);}), showNotification, showConsole, errors); } // check if the edge already exists in the graph, there is no point in a duplicate - for (const edge of eagle.logicalGraph().getEdges()){ + for (const edge of logicalGraph.getEdges()){ if (edge.getSrcPortId() === sourcePortId && edge.getDestPortId() === destinationPortId && edge.getId() !== edgeId){ - const x = Errors.ShowFix("Edge is a duplicate. Another edge with the same source port and destination port already exists", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixDeleteEdge(eagle, edgeId);}, "Delete edge"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const x = ActionMessage.ShowFix(ActionMessage.Level.Error, "Edge is a duplicate. Another edge with the same source port and destination port already exists", function(){Utils.showEdge(edgeId);}, function(){Utils.fixDeleteEdge(logicalGraph, edgeId);}, "Delete edge"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } } // check that all edges have same data type as their source and destination ports if (sourcePort !== null && !Utils.typesMatch(dataType, sourcePort.getType())){ - const x = Errors.ShowFix("Edge data type (" + dataType + ") does not match start port (" + sourcePort.getDisplayText() + ") data type (" + sourcePort.getType() + ").", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.fixEdgeType(eagle, edgeId, sourcePort.getType());}, "Change edge data type to match source port type"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const edge: Edge = logicalGraph.findEdgeById(edgeId); + + const x = ActionMessage.ShowFix(ActionMessage.Level.Error, "Edge data type (" + dataType + ") does not match start port (" + sourcePort.getDisplayText() + ") data type (" + sourcePort.getType() + ").", function(){Utils.showEdge(edgeId)}, function(){Utils.fixEdgeType(edge, sourcePort.getType());}, "Change edge data type to match source port type"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } if (destinationPort !== null && !Utils.typesMatch(dataType, destinationPort.getType())){ - const x = Errors.ShowFix("Edge data type (" + dataType + ") does not match end port (" + destinationPort.getDisplayText() + ") data type (" + destinationPort.getType() + ").", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.fixEdgeType(eagle, edgeId, destinationPort.getType());}, "Change edge data type to match destination port type"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const edge: Edge = logicalGraph.findEdgeById(edgeId); + + const x = ActionMessage.ShowFix(ActionMessage.Level.Error, "Edge data type (" + dataType + ") does not match end port (" + destinationPort.getDisplayText() + ") data type (" + destinationPort.getType() + ").", function(){Utils.showEdge(edgeId)}, function(){Utils.fixEdgeType(edge, destinationPort.getType());}, "Change edge data type to match destination port type"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } // check that all "closes loop" edges: @@ -467,44 +497,49 @@ export class Edge { // - destNode has a 'group_start' field set to true if (closesLoop){ if (!sourceNode.isData()){ - const x = Errors.Show("Closes Loop Edge (" + edgeId + ") does not start from a Data component.", function(){Utils.showEdge(eagle, edgeId);}); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const x = ActionMessage.Show(ActionMessage.Level.Error, "Closes Loop Edge (" + edgeId + ") does not start from a Data component.", function(){Utils.showEdge(edgeId);}); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } if (!destinationNode.isApplication()){ - const x = Errors.Show("Closes Loop Edge (" + edgeId + ") does not end at an Application component.", function(){Utils.showEdge(eagle, edgeId);}); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const x = ActionMessage.Show(ActionMessage.Level.Error, "Closes Loop Edge (" + edgeId + ") does not end at an Application component.", function(){Utils.showEdge(edgeId);}); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } if (!sourceNode.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_END) || !Utils.asBool(sourceNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_END).getValue())){ - const x = Errors.ShowFix("'Closes Loop' Edge (" + edgeId + ") start node (" + sourceNode.getName() + ") does not have 'group_end' set to true.", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixFieldValue(eagle, sourceNode, Daliuge.groupEndField, "true")}, "Set 'group_end' to true"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const x = ActionMessage.ShowFix(ActionMessage.Level.Error, "'Closes Loop' Edge (" + edgeId + ") start node (" + sourceNode.getName() + ") does not have '" + Daliuge.FieldName.GROUP_END + "' set to true.", function(){Utils.showEdge(edgeId);}, function(){Utils.fixFieldValue(sourceNode, Daliuge.groupEndField, "true")}, "Set '" + Daliuge.FieldName.GROUP_END + "' to true"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } if (!destinationNode.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_START) || !Utils.asBool(destinationNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_START).getValue())){ - const x = Errors.ShowFix("'Closes Loop' Edge (" + edgeId + ") end node (" + destinationNode.getName() + ") does not have 'group_start' set to true.", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixFieldValue(eagle, destinationNode, Daliuge.groupStartField, "true")}, "Set 'group_start' to true"); - Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errorsWarnings); + const x = ActionMessage.ShowFix(ActionMessage.Level.Error, "'Closes Loop' Edge (" + edgeId + ") end node (" + destinationNode.getName() + ") does not have '" + Daliuge.FieldName.GROUP_START + "' set to true.", function(){Utils.showEdge(edgeId);}, function(){Utils.fixFieldValue(destinationNode, Daliuge.groupStartField, "true")}, "Set '" + Daliuge.FieldName.GROUP_START + "' to true"); + Edge.isValidLog(edgeId, Eagle.LinkValid.Invalid, x, showNotification, showConsole, errors); } } - return Utils.worstEdgeError(errorsWarnings); + return ActionList.worstError(errors); } - private static isValidLog = (edgeId : string, linkValid : Eagle.LinkValid, issue: Errors.Issue, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : void => { + // TODO: may not need linkValid arg here, we can probably just use issue.level + private static isValidLog = (edgeId : string, linkValid : Eagle.LinkValid, issue: ActionMessage, showNotification : boolean, showConsole : boolean, errors: ActionMessage[]) : void => { // determine correct title let title = "Edge Valid"; - let type : "success" | "info" | "warning" | "danger" = "success"; + let level : ActionMessage.Level = ActionMessage.Level.Success; let message = ""; switch (linkValid){ case Eagle.LinkValid.Warning: - title = "Edge Warning"; - type = "warning"; - break; + title = "Edge Warning"; + level = ActionMessage.Level.Warning; + break; + case Eagle.LinkValid.Impossible: + title = "Edge Impossible"; + level = ActionMessage.Level.Error; + break; case Eagle.LinkValid.Invalid: - title = "Edge Invalid"; - type = "danger"; - break; + title = "Edge Invalid"; + level = ActionMessage.Level.Error; + break; } // add edge id to message, if id is known @@ -516,14 +551,14 @@ export class Edge { // add log message to correct location(s) if (showNotification) - Utils.showNotification(title, message, type); + Utils.showNotification(title, message, ActionMessage.levelToCss(level)); if (showConsole) console.warn(title + ":" + message); - if (type === "danger" && errorsWarnings !== null){ - errorsWarnings.errors.push(issue); + if (level === ActionMessage.Level.Error && errors !== null){ + errors.push(issue); } - if (type === "warning" && errorsWarnings !== null){ - errorsWarnings.warnings.push(issue); + if (level === ActionMessage.Level.Warning && errors !== null){ + errors.push(issue); } } } diff --git a/src/Errors.ts b/src/Errors.ts deleted file mode 100644 index 9713f6d50..000000000 --- a/src/Errors.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as ko from "knockout"; - -import {Eagle} from './Eagle'; -import {Setting} from './Setting'; -import {Utils} from './Utils'; - -export class Errors { - static Message(message: string): Errors.Issue { - return {message: message, show: null, fix: null, fixDescription:""}; - } - static Show(message: string, show: () => void): Errors.Issue { - return {message: message, show: show, fix: null, fixDescription:""}; - } - static Fix(message: string, fix: () => void, fixDescription: string): Errors.Issue { - return {message: message, show: null, fix: fix, fixDescription: fixDescription}; - } - static ShowFix(message: string, show: () => void, fix: () => void, fixDescription: string): Errors.Issue { - return {message: message, show: show, fix: fix, fixDescription: fixDescription}; - } - - static fixAll = () : void => { - const eagle: Eagle = Eagle.getInstance(); - const initialNumWarnings = eagle.graphWarnings().length; - const initialNumErrors = eagle.graphErrors().length; - let numErrors = Infinity; - let numWarnings = Infinity; - let numIterations = 0; - - while (numWarnings !== eagle.graphWarnings().length || numErrors !== eagle.graphErrors().length){ - if (numIterations > 10){ - console.warn("Too many iterations in fixAll()"); - break; - } - numIterations = numIterations+1; - - numWarnings = eagle.graphWarnings().length; - numErrors = eagle.graphErrors().length; - - for (const error of eagle.graphErrors()){ - if (error.fix !== null){ - error.fix(); - } - } - - for (const warning of eagle.graphWarnings()){ - if (warning.fix !== null){ - warning.fix(); - } - } - - eagle.checkGraph(); - } - - // show notification - Utils.showNotification("Fix All Graph Errors", initialNumErrors + " error(s), " + numErrors + " remain. " + initialNumWarnings + " warning(s), " + numWarnings + " remain.", "info"); - - Utils.postFixFunc(eagle); - } - - static hasWarnings = (errorsWarnings: Errors.ErrorsWarnings) : boolean => { - return errorsWarnings.warnings.length > 0; - } - - static hasErrors = (errorsWarnings: Errors.ErrorsWarnings) : boolean => { - return errorsWarnings.errors.length > 0; - } - - static getWarnings : ko.PureComputed = ko.pureComputed(() => { - const eagle: Eagle = Eagle.getInstance(); - - switch (eagle.errorsMode()){ - case Setting.ErrorsMode.Loading: - return eagle.loadingWarnings(); - case Setting.ErrorsMode.Graph: - return eagle.graphWarnings(); - default: - console.warn("Unknown errorsMode (" + eagle.errorsMode() + "). Unable to getWarnings()"); - return []; - } - }, this); - - static getErrors : ko.PureComputed = ko.pureComputed(() => { - const eagle: Eagle = Eagle.getInstance(); - - switch (eagle.errorsMode()){ - case Setting.ErrorsMode.Loading: - return eagle.loadingErrors(); - case Setting.ErrorsMode.Graph: - return eagle.graphErrors(); - default: - console.warn("Unknown errorsMode (" + eagle.errorsMode() + "). Unable to getErrors()"); - return []; - } - }, this); - - static getNumFixableIssues : ko.PureComputed = ko.pureComputed(() => { - let count: number = 0; - const errors: Errors.Issue[] = Errors.getErrors(); - const warnings: Errors.Issue[] = Errors.getWarnings(); - - // count the errors - for (const error of errors){ - if (error.fix !== null){ - count += 1; - } - } - - // count the warnings - for (const warning of warnings){ - if (warning.fix !== null){ - count += 1; - } - } - - return count; - }, this); -} - -export namespace Errors -{ - export type Issue = {message: string, show: () => void, fix: () => void, fixDescription: string}; - export type ErrorsWarnings = {warnings: Issue[], errors: Issue[]}; -} diff --git a/src/ExplorePalettes.ts b/src/ExplorePalettes.ts index 19a8f4375..0bda182f3 100644 --- a/src/ExplorePalettes.ts +++ b/src/ExplorePalettes.ts @@ -104,7 +104,7 @@ export class ExplorePalettes { if (typeof event === "undefined"){ // load immediately - eagle.openRemoteFile(new RepositoryFile(new Repository(data.repositoryService, data.repositoryName, data.repositoryBranch, false), data.path, data.name)); + eagle.openRemoteFile(new RepositoryFile(new Repository(data.repositoryService, data.repositoryName, data.repositoryBranch, false), data.path, data.name), null); $('#explorePalettesModal').modal('hide'); } else { const newState = !data.isSelected() diff --git a/src/Field.ts b/src/Field.ts index 1520a818e..d3ffe8927 100644 --- a/src/Field.ts +++ b/src/Field.ts @@ -23,6 +23,17 @@ export class Field { private isEvent : ko.Observable; private nodeKey : ko.Observable; + // graph related attributes + private inputX : ko.Observable; + private inputY : ko.Observable; + private outputX : ko.Observable; + private outputY : ko.Observable; + private peek : ko.Observable; + private inputConnected : ko.Observable + private outputConnected : ko.Observable + private inputAngle : number; + private outputAngle : number; + constructor(id: string, displayText: string, value: string, defaultValue: string, description: string, readonly: boolean, type: string, precious: boolean, options: string[], positional: boolean, parameterType: Daliuge.FieldType, usage: Daliuge.FieldUsage, keyAttribute: boolean){ this.displayText = ko.observable(displayText); this.value = ko.observable(value); @@ -40,6 +51,17 @@ export class Field { this.usage = ko.observable(usage); this.isEvent = ko.observable(false); this.nodeKey = ko.observable(0); + + //graph related things + this.inputX = ko.observable(0); + this.inputY = ko.observable(0); + this.outputX = ko.observable(0); + this.outputY = ko.observable(0); + this.peek = ko.observable(false); + this.inputConnected = ko.observable(false) + this.outputConnected = ko.observable(false) + this.inputAngle = 0; + this.outputAngle = 0; } getId = () : string => { @@ -90,6 +112,44 @@ export class Field { return this.description() == "" ? "No description available" + " (" + this.type() + ", default value:'" + this.defaultValue() + "')" : this.description() + " (" + this.type() + ", default value:'" + this.defaultValue() + "')"; }, this); + getInputPosition = () : {x:number, y:number} => { + return {x: this.inputX(), y: this.inputY()}; + } + + getOutputPosition = () : {x:number, y:number} => { + return {x: this.outputX(), y: this.outputY()}; + } + + setInputPosition = (x: number, y: number) : void => { + this.inputX(x); + this.inputY(y); + } + + setOutputPosition = (x: number, y: number) : void => { + this.outputX(x); + this.outputY(y); + } + + setInputAngle = (angle:number) : void => { + this.inputAngle = angle + } + + getInputAngle = () : number => { + return this.inputAngle + } + + flagInputAngleMutated = () : void => { + this.displayText.valueHasMutated() + } + + setOutputAngle = (angle:number) :void => { + this.outputAngle = angle + } + + getOutputAngle = () : number => { + return this.outputAngle + } + isReadonly = () : boolean => { return this.readonly(); } @@ -457,6 +517,35 @@ export class Field { return ""; } + getHelpHtml= () : string => { + return "###"+ this.getDisplayText() + "\n" + this.getDescription(); + + } + + isPeek = () : boolean => { + return this.peek() + } + + setPeek = (value:boolean) : void => { + this.peek(value); + } + + getInputConnected = () :boolean => { + return this.inputConnected() + } + + setInputConnected = (value:boolean) : void => { + this.inputConnected(value) + } + + getOutputConnected = () :boolean => { + return this.outputConnected() + } + + setOutputConnected = (value:boolean) : void => { + this.outputConnected(value) + } + // used to transform the value attribute of a field into a variable with the correct type // the value attribute is always stored as a string internally static stringAsType = (value: string, type: string) : any => { @@ -571,12 +660,20 @@ export class Field { // handle legacy fieldType if (typeof data.fieldType !== 'undefined'){ switch (data.fieldType){ + case "ApplicationArgument": + parameterType = Daliuge.FieldType.ApplicationArgument; + usage = Daliuge.FieldUsage.NoPort; + break; case "ComponentParameter": parameterType = Daliuge.FieldType.ComponentParameter; usage = Daliuge.FieldUsage.NoPort; break; - case "ApplicationArgument": - parameterType = Daliuge.FieldType.ApplicationArgument; + case "ConstraintParameter": + parameterType = Daliuge.FieldType.ConstraintParameter; + usage = Daliuge.FieldUsage.NoPort; + break; + case "ConstructParameter": + parameterType = Daliuge.FieldType.ConstructParameter; usage = Daliuge.FieldUsage.NoPort; break; case "InputPort": @@ -587,10 +684,6 @@ export class Field { parameterType = Daliuge.FieldType.ApplicationArgument; usage = Daliuge.FieldUsage.OutputPort; break; - case "ConstructParameter": - parameterType = Daliuge.FieldType.ConstructParameter; - usage = Daliuge.FieldUsage.NoPort; - break; default: console.warn("Unhandled fieldType", data.fieldType); } diff --git a/src/FileInfo.ts b/src/FileInfo.ts index 6aabafd42..f8b35d0f7 100644 --- a/src/FileInfo.ts +++ b/src/FileInfo.ts @@ -1,8 +1,8 @@ import * as ko from "knockout"; -import { Daliuge } from "./Daliuge"; +import { ActionMessage } from "./Action"; import { Eagle } from './Eagle'; -import { Errors } from './Errors'; +import { Daliuge } from "./Daliuge"; import { Utils } from './Utils'; @@ -425,7 +425,7 @@ export class FileInfo { } // TODO: use errors array if attributes cannot be found - static fromOJSJson = (modelData : any, errorsWarnings: Errors.ErrorsWarnings) : FileInfo => { + static fromOJSJson = (modelData : any, errors: ActionMessage[]) : FileInfo => { const result : FileInfo = new FileInfo(); result.path = Utils.getFilePathFromFullPath(modelData.filePath); @@ -463,7 +463,7 @@ export class FileInfo { // check that lastModifiedDatetime is a Number, if not correct if (typeof result.lastModifiedDatetime !== 'number'){ result.lastModifiedDatetime = 0; - errorsWarnings.errors.push(Errors.Message("Last Modified Datetime contains string instead of number, resetting to default (0). Please save this graph to update lastModifiedDatetime to a correct value.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Last Modified Datetime contains string instead of number, resetting to default (0). Please save this graph to update lastModifiedDatetime to a correct value.")); } result.numLGNodes = modelData.numLGNodes == undefined ? 0 : modelData.numLGNodes; diff --git a/src/GitHub.ts b/src/GitHub.ts index 68a4d0735..cb185bdee 100644 --- a/src/GitHub.ts +++ b/src/GitHub.ts @@ -215,9 +215,8 @@ export class GitHub { /** * Gets the specified remote file from the server - * @param filePath File path. */ - static openRemoteFile(repositoryService : Eagle.RepositoryService, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string, callback: (error : string, data : string) => void ) : void { + static openRemoteFile(file: RepositoryFile, callback: (error : string, data : string) => void ) : void { const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY); if (token === null || token === "") { @@ -225,13 +224,13 @@ export class GitHub { return; } - const fullFileName : string = Utils.joinPath(filePath, fileName); + const fullFileName : string = Utils.joinPath(file.path, file.name); // Add parameters in json data. const jsonData = { - repositoryName: repositoryName, - repositoryBranch: repositoryBranch, - repositoryService: repositoryService, + repositoryName: file.repository.name, + repositoryBranch: file.repository.branch, + repositoryService: file.repository.service, token: token, filename: fullFileName }; diff --git a/src/GitLab.ts b/src/GitLab.ts index 33fab5fe8..cc9d5dd68 100644 --- a/src/GitLab.ts +++ b/src/GitLab.ts @@ -182,7 +182,7 @@ export class GitLab { * Gets the specified remote file from the server * @param filePath File path. */ - static openRemoteFile(repositoryService : Eagle.RepositoryService, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string, callback: (error : string, data : string) => void ) : void { + static openRemoteFile(file: RepositoryFile, callback: (error : string, data : string) => void ) : void { const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY); if (token === null || token === "") { @@ -190,13 +190,13 @@ export class GitLab { return; } - const fullFileName : string = Utils.joinPath(filePath, fileName); + const fullFileName : string = Utils.joinPath(file.path, file.name); // Add parameters in json data. const jsonData = { - repositoryName: repositoryName, - repositoryBranch: repositoryBranch, - repositoryService: repositoryService, + repositoryName: file.repository.name, + repositoryBranch: file.repository.branch, + repositoryService: file.repository.service, token: token, filename: fullFileName }; diff --git a/src/GraphChecker.ts b/src/GraphChecker.ts new file mode 100644 index 000000000..611cf63fd --- /dev/null +++ b/src/GraphChecker.ts @@ -0,0 +1,195 @@ +import * as ko from "knockout"; + +import { ActionMessage } from "./Action"; +import { Eagle } from './Eagle'; +import { Edge } from "./Edge"; +import { LogicalGraph } from "./LogicalGraph"; +import { Node } from "./Node"; +import { Utils } from './Utils'; + +export class GraphChecker { + + issues: ko.ObservableArray; + + constructor(){ + this.issues = ko.observableArray([]); + } + + static check(logicalGraph: LogicalGraph){ + const errors: ActionMessage[] = []; + const eagle: Eagle = Eagle.getInstance(); + + // check all nodes are valid + for (const node of logicalGraph.getNodes()){ + Node.isValid(eagle, node, Eagle.selectedLocation(), false, false, errors); + } + + // check all edges are valid + for (const edge of logicalGraph.getEdges()){ + Edge.isValid(logicalGraph, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, false, errors); + } + + return errors; + } + + // TODO: should we pass eagle as an argument here? + check = () : void => { + const eagle = Eagle.getInstance(); + const logicalGraph: LogicalGraph = eagle.logicalGraph(); + const errors: ActionMessage[] = GraphChecker.check(logicalGraph); + this.issues(errors); + } + + static fix = (index: number): void => { + console.log("GraphChecker.fix()", index); + + const eagle: Eagle = Eagle.getInstance(); + const graphChecker: GraphChecker = eagle.graphChecker(); + + console.log("graphChecker.issues().length", graphChecker.issues().length); + + const issue = graphChecker.issues()[index]; + if (issue.fix !== null){ + // perform the action + issue.fix(); + + // and remove it from the list + graphChecker.issues.splice(index, 1); + } + + this.postFixFunc(eagle); + } + + static fixNode = (index: number): void => { + console.log("GraphChecker.fixNode()", index); + } + + // TODO: should we pass eagle as an argument here? + static fixAll = () : void => { + const eagle: Eagle = Eagle.getInstance(); + const graphChecker: GraphChecker = eagle.graphChecker(); + const initialNumIssues = graphChecker.issues().length; + let numIssues = Infinity; + let numIterations = 0; + + // iterate through the messages list multiple times, until the length of the list is unchanged + while (numIssues !== graphChecker.issues().length){ + // check that we haven't iterated through the list too many times + if (numIterations > 10){ + console.warn("Too many iterations in fixAll()"); + break; + } + numIterations = numIterations+1; + + numIssues = graphChecker.issues().length; + + for (let i = numIssues - 1 ; i >= 0 ; i--){ + const issue = graphChecker.issues()[i]; + if (issue.fix !== null){ + // perform the action + issue.fix(); + + // and remove it from the list + graphChecker.issues.splice(i, 1); + } + } + } + + // show notification + Utils.showNotification("Fixed All Issues: ", initialNumIssues + " issue(s), " + numIssues + " remain. ", "info"); + + this.postFixFunc(eagle); + } + + static postFixFunc = (eagle: Eagle) => { + eagle.selectedObjects.valueHasMutated(); + eagle.logicalGraph().fileInfo().modified = true; + + eagle.graphChecker().check(); + + eagle.undo().pushSnapshot(eagle, "Fix"); + } + + getNumFixableIssues : ko.PureComputed = ko.pureComputed(() => { + let count: number = 0; + + // count the warnings + for (const message of this.issues()){ + if (message.fix !== null){ + count += 1; + } + } + + return count; + }, this); + + + getNumWarnings : ko.PureComputed = ko.pureComputed(() => { + let result: number = 0; + + for (const error of this.issues()){ + if (error.level === ActionMessage.Level.Warning){ + result += 1; + } + } + + return result; + + }, this); + + getNumErrors : ko.PureComputed = ko.pureComputed(() => { + let result: number = 0; + + for (const error of this.issues()){ + if (error.level === ActionMessage.Level.Error){ + result += 1; + } + } + + return result; + }, this); + + static hasWarnings = (errors: ActionMessage[]) : boolean => { + if (errors === null){ + return false; + } + + for (const error of errors){ + if (error.level === ActionMessage.Level.Warning){ + return true; + } + } + return false; + } + + static hasErrors = (errors: ActionMessage[]) : boolean => { + if (errors === null){ + return false; + } + + for (const error of errors){ + if (error.level === ActionMessage.Level.Error){ + return true; + } + } + return false; + } + + // only update result if it is worse that current result + static worstError(errors: ActionMessage[]) : Eagle.LinkValid { + // TODO: can probably avoid doing two loops here! + const hasWarnings: boolean = GraphChecker.hasWarnings(errors); + const hasErrors: boolean = GraphChecker.hasErrors(errors); + + if (!hasWarnings && !hasErrors){ + return Eagle.LinkValid.Valid; + } + + if (hasErrors){ + return Eagle.LinkValid.Invalid; + } + + return Eagle.LinkValid.Warning; + } +} + diff --git a/src/GraphRenderer.ts b/src/GraphRenderer.ts new file mode 100644 index 000000000..2b4d2058f --- /dev/null +++ b/src/GraphRenderer.ts @@ -0,0 +1,2181 @@ +/* +# +# ICRAR - International Centre for Radio Astronomy Research +# (c) UWA - The University of Western Australia, 2016 +# Copyright by UWA (in the framework of the ICRAR) +# All rights reserved +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# +*/ + +import * as ko from "knockout"; + +import { Category } from './Category'; +import { Daliuge } from "./Daliuge"; +import { Eagle } from './Eagle'; +import { Edge } from "./Edge"; +import { Field } from './Field'; +import { GraphConfig } from './graphConfig'; +import { LogicalGraph } from './LogicalGraph'; +import { Node } from './Node'; +import { Utils } from './Utils'; +import { CategoryData} from './CategoryData'; +import { Setting } from './Setting'; +import { RightClick } from "./RightClick"; + +ko.bindingHandlers.nodeRenderHandler = { + init: function(element:any, valueAccessor) { + const node: Node = ko.unwrap(valueAccessor()) + + //overwriting css variables using colours from graphConfig.ts. I am using this for simple styling to avoid excessive css data binds in the node html files + $("#logicalGraphParent").get(0).style.setProperty("--selectedBg", GraphConfig.getColor('selectBackground')); + $("#logicalGraphParent").get(0).style.setProperty("--selectedConstructBg", GraphConfig.getColor('selectConstructBackground')); + $("#logicalGraphParent").get(0).style.setProperty("--nodeBorder", GraphConfig.getColor('bodyBorder')); + $("#logicalGraphParent").get(0).style.setProperty("--nodeBg", GraphConfig.getColor('nodeBg')); + $("#logicalGraphParent").get(0).style.setProperty("--graphText", GraphConfig.getColor('graphText')); + $("#logicalGraphParent").get(0).style.setProperty("--branchBg", GraphConfig.getColor('branchBg')); + $("#logicalGraphParent").get(0).style.setProperty("--constructBg", GraphConfig.getColor('constructBg')); + $("#logicalGraphParent").get(0).style.setProperty("--embeddedApp", GraphConfig.getColor('embeddedApp')); + $("#logicalGraphParent").get(0).style.setProperty("--constructIcon", GraphConfig.getColor('constructIcon')); + $("#logicalGraphParent").get(0).style.setProperty("--commentEdgeColor", GraphConfig.getColor('commentEdge')); + $("#logicalGraphParent").get(0).style.setProperty("--matchingEdgeColor", GraphConfig.getColor('edgeAutoComplete')); + + if( node.isData()){ + $(element).find('.body').css('background-color:#575757','color:white') + } + }, + update: function (element:any, valueAccessor) { + const node: Node = ko.unwrap(valueAccessor()); + + // set size + $(element).css({'height':node.getRadius()*2+'px','width':node.getRadius()*2+'px'}); + + if(node.isLoop()){ + $(element).children().children().children('.body').css({'border-style':'dotted','border-width':'4px'}) + } + if(node.isGather()){ + $(element).children().children().children('.body').css({'border-style':'dashed','border-width':'5px'}) + } + if(node.isScatter()){ + $(element).children().children().children('.body').css({'border-style':'double','border-width':'7px'}) + } + if(node.isExclusiveForceNode()){ + $(element).children().children().children('.body').css({'background-color':'white'}) + } + + if(node.isGroup()|| node.getParentKey() != null){ + GraphRenderer.resizeConstruct(node,false) + } + }, +}; + +//port html elements will have a data bind to the port position saved on the field - This is an observable + +//save the calculated connection angle on the field (this is the ideal position) +//then check if this is abvailable using a node.getfields.getangle Loop +//if so, write it into the port position, this will trigger the html data-bind to draw/redraw the port +//if not available we add a set amount to the closest port's position, repeating until we find an available spot, saving each port we are colliding with +//this set amount is a dinstance we need to keep. we will have to calculate an angle based on the radius of a node to keep this distance +//we then do the same but subtracting the set amount +//use the lower distance +//figure out the mean angle of this 'port group' and center them. update all of their port positions + +//first dangling port, check for biggest gap +//check if half of the biggest gap is still bigger than the second biggest gap +//if so use the center of half of the biggest gap in input ports left bound, output right +//place the dangling port +//if another dangling port is updated, check if a dangling port of the same type has already been placed(input of output) +//if so have them share the space, if not enough space is available, find another spot + +ko.bindingHandlers.embeddedAppPosition = { + update: function (element:any, valueAccessor) { + const eagle : Eagle = Eagle.getInstance(); + const applicationNode: Node = ko.utils.unwrapObservable(valueAccessor()).applicationNode; + const input: boolean = ko.utils.unwrapObservable(valueAccessor()).input; + + // find the node in which the applicationNode has been embedded + const parentNode: Node = eagle.logicalGraph().findNodeByKeyQuiet(applicationNode.getEmbedKey()); + + // determine all the adjacent nodes + // TODO: earlier abort if field is null + const adjacentNodes: Node[] = GraphRenderer.getAdjacentNodes(applicationNode, input); + const connectedField: boolean = adjacentNodes.length > 0; + + // for branch nodes the ports are inset from the outer radius a little bit in their design + const parentNodeRadius = parentNode.getRadius(); + + // determine port position + const parentNodePosition = parentNode.getPosition(); + let portPosition; + + if(connectedField){ + // calculate angles to all adjacent nodes + const angles: number[] = []; + for (const adjacentNode of adjacentNodes){ + const adjacentNodePos = adjacentNode.getPosition() + const edgeAngle = GraphRenderer.calculateConnectionAngle(parentNodePosition, adjacentNodePos) + angles.push(edgeAngle); + } + + // average the angles + const averageAngle = GraphRenderer.averageAngles(angles); + portPosition = GraphRenderer.calculatePortPos(averageAngle, parentNodeRadius, parentNodeRadius) + }else{ + // find a default position for the port when not connected + if (input){ + portPosition=GraphRenderer.calculatePortPos(Math.PI, parentNodeRadius, parentNodeRadius) + } else { + portPosition=GraphRenderer.calculatePortPos(0, parentNodeRadius, parentNodeRadius) + } + } + + // we are saving the embedded application's position data here using the offset we calculated + const newPos = { + x: parentNodePosition.x - parentNodeRadius + portPosition.x, + y: parentNodePosition.y - parentNodeRadius + portPosition.y + } + applicationNode.setPosition(newPos.x,newPos.y) + + portPosition = { + x: portPosition.x - parentNodeRadius, + y: portPosition.y - parentNodeRadius + } + + // applying the offset to the element + $(element).css({ + 'top': portPosition.y+'px', + 'left':portPosition.x+'px' + }); + } +}; + +ko.bindingHandlers.graphRendererPortPosition = { + update: function (element:any, valueAccessor) { + //this handler is for a PORT position, meaning it will run twice for a field that has both input and output ports + + //the update function is called initially and then whenever a change to a utilised observable occurs + const eagle : Eagle = Eagle.getInstance(); + const n: Node = ko.utils.unwrapObservable(valueAccessor()).n; + const f: Field = ko.utils.unwrapObservable(valueAccessor()).f; + const dataType: string = ko.utils.unwrapObservable(valueAccessor()).type; + // determine the 'node' and 'field' attributes (for this way of using this binding) + let node : Node + let field : Field + + switch(dataType){ + case 'inputPort': + case 'outputPort': + node = eagle.logicalGraph().findNodeByKeyQuiet(f.getNodeKey()) + field = f + break; + case 'comment': + node = n + field = null + break; + } + + // determine all the adjacent nodes + const adjacentNodes: Node[] = []; + let connectedField:boolean=false; + + switch(dataType){ + case 'comment': + const adjacentNode: Node = eagle.logicalGraph().findNodeByKeyQuiet(n.getSubjectKey()); + + if (adjacentNode === null){ + console.warn("Could not find adjacentNode for comment with subjectKey", n.getSubjectKey()); + return; + } + + adjacentNodes.push(adjacentNode); + break; + + case 'inputPort': + for(const edge of eagle.logicalGraph().getEdges()){ + if(field != null && field.getId()===edge.getDestPortId()){ + const adjacentNode: Node = eagle.logicalGraph().findNodeByKeyQuiet(edge.getSrcNodeKey()); + + if (adjacentNode === null){ + console.warn("Could not find adjacentNode for inputPort or inputApp with SrcNodeKey", edge.getSrcNodeKey()); + return; + } + + connectedField=true + adjacentNodes.push(adjacentNode); + continue; + } + } + break; + + case 'outputPort': + for(const edge of eagle.logicalGraph().getEdges()){ + if(field.getId()===edge.getSrcPortId()){ + const adjacentNode: Node = eagle.logicalGraph().findNodeByKeyQuiet(edge.getDestNodeKey()); + + if (adjacentNode === null){ + console.warn("Could not find adjacentNode for outputPort or outputApp with DestNodeKey", edge.getDestNodeKey()); + return; + } + + connectedField=true + adjacentNodes.push(adjacentNode); + continue; + } + } + break; + } + + // get node radius + const nodeRadius = node.getRadius() + // determine port position + const currentNodePos = node.getPosition(); + let portPosition; + let averageAngle + + if(connectedField || dataType === 'comment'){ + + // calculate angles to all adjacent nodes + const angles: number[] = []; + for (const adjacentNode of adjacentNodes){ + const adjacentNodePos = adjacentNode.getPosition() + const edgeAngle = GraphRenderer.calculateConnectionAngle(currentNodePos, adjacentNodePos) + angles.push(edgeAngle); + } + + // average the angles + averageAngle = GraphRenderer.averageAngles(angles); + if(averageAngle<0){ + averageAngle = Math.PI*2 - Math.abs(averageAngle) + } + + if (dataType === 'inputPort'){ + field.setInputAngle(averageAngle) + } else if (dataType === 'outputPort'){ + field.setOutputAngle(averageAngle) + } + }else{ + // find a default position for the port when not connected + switch (dataType){ + case 'inputPort': + // portPosition=GraphRenderer.calculatePortPos(Math.PI, nodeRadius, nodeRadius) + averageAngle = 3.14159 + field.setInputAngle(averageAngle) + break; + case 'outputPort': + // portPosition=GraphRenderer.calculatePortPos(0, nodeRadius, nodeRadius) + averageAngle = 0 + field.setOutputAngle(averageAngle) + break; + default: + console.warn("disconnected field with dataType:", dataType); + portPosition=GraphRenderer.calculatePortPos(Math.PI/2, nodeRadius, nodeRadius) + break; + } + } + //checking for port colisions if connected + if(!node.isComment()){ + //calculating the minimum port distance as an angle. we save this min distance as a pixel distance between ports + const minimumPortDistance:number = Number(Math.asin(GraphConfig.PORT_MINIMUM_DISTANCE/node.getRadius()).toFixed(6)) + + //checking if the ports are linked + const portIsLinked = eagle.logicalGraph().portIsLinked(node.getKey(),field.getId()) + field.setInputConnected(portIsLinked.input) + field.setOutputConnected(portIsLinked.output) + + if(dataType === 'inputPort'){//for input ports + const newInputPortAngle = GraphRenderer.findClosestMatchingAngle(node,field.getInputAngle(),minimumPortDistance,field,'input') + field.setInputAngle(newInputPortAngle) + } + + if(dataType === 'outputPort'){//for output ports + const newOutputPortAngle = GraphRenderer.findClosestMatchingAngle(node,field.getOutputAngle(),minimumPortDistance,field,'output') + field.setOutputAngle(newOutputPortAngle) + } + } + + if (dataType === 'inputPort'){ + portPosition = GraphRenderer.calculatePortPos(field.getInputAngle(), nodeRadius, nodeRadius) + //a little 1px reduction is needed to center ports for some reason + if(!node.isBranch()){ + portPosition = {x:portPosition.x-1,y:portPosition.y-1} + } + + field.setInputPosition(portPosition.x, portPosition.y); + } + if (dataType === 'outputPort'){ + portPosition = GraphRenderer.calculatePortPos(field.getOutputAngle(), nodeRadius, nodeRadius) + + //a little 1px reduction is needed to center ports for some reason + if(!node.isBranch()){ + portPosition = {x:portPosition.x-1,y:portPosition.y-1} + } + + field.setOutputPosition(portPosition.x, portPosition.y); + } + + //align the port titles to the correct side of the node, depending on node angle + //clear style since it doesnt seem to overwrite + $(element).find(".portTitle").removeAttr( "style" ) + + //convert negative radian angles to positive + if(averageAngle<0){ + averageAngle= averageAngle+2*Math.PI + } + + //apply the correct css + if(averageAngle>1.5708 && averageAngle<4.7123){ + $(element).find(".portTitle").css({'text-align':'right','left':-5+'px','transform':'translateX(-100%)'}) + }else{ + $(element).find(".portTitle").css({'text-align':'left','right':-5+'px','transform':'translateX(100%)'}) + } + } +}; + +export class GraphRenderer { + static nodeData : Node[] = null + + //port drag handler globals + static draggingPort : boolean = false; + static isDraggingPortValid: ko.Observable = ko.observable("Unknown"); + static destinationNode : Node = null; + static destinationPort : Field = null; + + static portDragSourceNode : ko.Observable = ko.observable(null); + static portDragSourcePort : ko.Observable = ko.observable(null); + static portDragSourcePortIsInput: boolean = false; + + static portDragSuggestedNode : ko.Observable = ko.observable(null); + static portDragSuggestedField : ko.Observable = ko.observable(null); + static matchingPortList : {field:Field,node:Node}[] = [] + static portMatchCloseEnough :ko.Observable = ko.observable(false); + + //node drag handler globals + static NodeParentRadiusPreDrag : number = null; + static nodeDragElement : any = null + static nodeDragNode : Node = null + static dragStartPosition : any = null + static dragCurrentPosition : any = null + static dragSelectionHandled : any = ko.observable(true) + static dragSelectionDoubleClick :boolean = false; + + //drag selection region globals + static altSelect : boolean = false; + static shiftSelect : boolean = false; + static isDraggingSelectionRegion :boolean = false; + static selectionRegionStart = {x:0, y:0}; + static selectionRegionEnd = {x:0, y:0}; + + static mousePosX : ko.Observable = ko.observable(-1); + static mousePosY : ko.Observable = ko.observable(-1); + static legacyGraph : boolean = false; //used for marking a graph when its nodes don't have a radius set. in this case we will do some conversion + + static renderDraggingPortEdge : ko.Observable = ko.observable(false); + + static averageAngles(angles: number[]) : number { + let x: number = 0; + let y: number = 0; + + for (const angle of angles) { + x += Math.cos(angle) + y += Math.sin(angle) + } + + return Math.atan2(y, x); + } + + static calculatePortPositionX (mode : string, field : Field, node : Node) : number { + + let portPosX :number + if(mode==='input'){ + portPosX = field.getInputPosition().x + }else{ + portPosX = field.getOutputPosition().x + } + + const x = portPosX + node.getPosition().x - node.getRadius() + return x + } + + static calculatePortPositionY (mode:string, field : Field, node : Node) { + + let portPosY :number + if(mode==='input'){ + portPosY = field.getInputPosition().y + }else{ + portPosY = field.getOutputPosition().y + } + + const y = portPosY + node.getPosition().y - node.getRadius() + return y + } + + static findClosestMatchingAngle (node:Node, angle:number, minPortDistance:number,field:Field,mode:string) : number { + let result = 0 + let minAngle + let maxAngle + + let currentAngle = angle + let noMatch = true + let cicles = 0 + + //checking max angle + while(noMatch && cicles<10){ + const collidingPortAngle:number = GraphRenderer.checkForPortUsingAngle(node,currentAngle,minPortDistance, field,mode) + if(collidingPortAngle === null){ + maxAngle = currentAngle //weve found our closest gap when adding to our angle + noMatch = false + }else{ + //if the colliding angle is not 0, that means the checkForPortUsingAngle function has found and returned the angle of a port we are colliding with + //we will use this colliding port angle and add the minimum port distance as well as a little extra to prevent math errors when comparing + currentAngle = collidingPortAngle + minPortDistance + 0.01 + + if(currentAngle<0){ + currentAngle = 2*Math.PI - Math.abs(currentAngle) + } + + cicles++ + } + } + + //resetting runtime vars + noMatch = true + cicles = 0 + currentAngle = angle + + //checking min angle + while(noMatch && cicles<10){ + const collidingPortAngle:number = GraphRenderer.checkForPortUsingAngle(node,currentAngle,minPortDistance, field,mode) + if(collidingPortAngle === null){ + minAngle = currentAngle //weve found our closest gap when adding to our angle + noMatch = false + }else{ + //if the colliding angle is not 0, that means the checkForPortUsingAngle function has found and returned the angle of a port we are colliding with + //we will use this colliding port angle and subtract the minimum port distance as well as a little extra to prevent math errors when comparing + currentAngle = collidingPortAngle - minPortDistance - 0.01 + + if(currentAngle<0){ + currentAngle = 2*Math.PI - Math.abs(currentAngle) + } + + cicles++ + } + } + + //maxing sure min and max angles are on the same side of the 0 point eg. if max angle is 0.2 and min angle is 5.8 we need to convert the min angle to be a negative number in order to compare them by subtracting 2*PI + if(minAngle + minPortDistance> 2*Math.PI && angle - minPortDistance < 0){ + minAngle = minAngle - 2*Math.PI + } + if(maxAngle - minPortDistance < 0 && angle + minPortDistance > 2*Math.PI){ + maxAngle = 2*Math.PI - maxAngle + } + + //checking if the min or max angle is closer to the port's preferred location + if(Math.abs(minAngle-angle)>Math.abs(maxAngle-angle)){ + result = maxAngle + }else{ + result = minAngle + } + + //making sure the angle is within the 0 - 2*PI range + if(minAngle<0){ + minAngle = 2*Math.PI - Math.abs(minAngle) + } + if(maxAngle>2*Math.PI){ + maxAngle = maxAngle - 2*Math.PI + } + + return result + } + + static checkForPortUsingAngle (node:Node, angle:number, minPortDistance:number, activeField:Field,mode:string) : number { + //we check if there are any ports within range of the desired angle. if there are we will return the angle of the port we collided with + let result:number = null + + //dangling ports will collide with all other ports including other dandling ports, connected ports take priority and will push dangling ones out of the way + let danglingActivePort = false + if(mode === "input"){ + danglingActivePort = !activeField.getInputConnected() + } + if( mode === "output"){ + danglingActivePort = !activeField.getOutputConnected() + } + + node.getFields().forEach(function(field){ + //going through all fields on the node to check for taken angles + + //making sure the field we are looking at is a port + if(!field.isInputPort() && !field.isOutputPort()){ + return + } + + //if the result is not null that means we are colliding with a port, there is no reason to continue checking + if( result != null){ + return + } + + + //either comparing with other connected ports || if the active port is dangling, compare with all other ports + if(field.getOutputConnected() || danglingActivePort){ + let fieldAngle = field.getOutputAngle() + //doing some converting if the ports we are comparing are on either side of the 0 point eg. 0.2 and 6.1 are too close to each other, we convert 6.1 - 2* PI = roughly -0.18. now we can compare them + if(fieldAngle - minPortDistance<0 && angle + minPortDistance>2 * Math.PI){ + fieldAngle = 2 * Math.PI + fieldAngle + }else if(fieldAngle + minPortDistance>2 * Math.PI && angle - minPortDistance<0){ + fieldAngle = fieldAngle - 2*Math.PI + } + + if(field.getId() === activeField.getId() && mode === 'output'){ + //this is the same exact port, dont compare! + }else{ + if(fieldAngle-angle > -minPortDistance && fieldAngle-angle < minPortDistance || field.getOutputAngle()-angle > -minPortDistance && field.getOutputAngle()-angle < minPortDistance){ + //we have found a port that is within the minimum port dinstance, return the angle of the port we are colliding with + result = field.getOutputAngle() + if(!danglingActivePort && field.getInputConnected() === false){ + field.flagInputAngleMutated() + } + return + } + } + } + + if(field.getInputConnected() || danglingActivePort){ + let fieldAngle = field.getInputAngle() + //doing some converting if the ports we are comparing are on either side of the 0 point eg. 0.2 and 6.1 are too close to each other, we convert 6.1 - 2* PI = roughly -0.18. now we can compare them + if(fieldAngle - minPortDistance<0 && angle + minPortDistance>2 * Math.PI){ + fieldAngle = 2 * Math.PI + fieldAngle + }else if(fieldAngle + minPortDistance>2 * Math.PI && angle - minPortDistance<0){ + fieldAngle = fieldAngle - 2*Math.PI + } + + if(field.getId() === activeField.getId() && mode === 'input'){ + //this is the same exact port, dont compare! + }else{ + if(fieldAngle-angle > -minPortDistance && fieldAngle-angle < minPortDistance || field.getInputAngle()-angle > -minPortDistance && field.getInputAngle()-angle < minPortDistance){ + //we have found a port that is within the minimum port dinstance, return the angle of the port we are colliding with + result = field.getInputAngle() + if(!danglingActivePort){ + field.flagInputAngleMutated() + } + return + } + } + } + }) + + return result + } + + static getAdjacentNodes(node: Node, input: boolean): Node[] { + const eagle: Eagle = Eagle.getInstance(); + + // find a single port of the correct type to consider when looking for adjacentNodes + // TODO: why do we select a single port here, why not consider all ports (if multiple exist)? + let field : Field; + for(const port of node.getFields()){ + if (input && port.isInputPort()){ + field = port; + break; + } + if (!input && port.isOutputPort()){ + field = port; + break; + } + } + + // determine all the adjacent nodes + // TODO: earlier abort if field is null + const adjacentNodes: Node[] = []; + + if (input){ + for(const edge of eagle.logicalGraph().getEdges()){ + if(field != null && field.getId()===edge.getDestPortId()){ + const adjacentNode: Node = eagle.logicalGraph().findNodeByKeyQuiet(edge.getSrcNodeKey()); + adjacentNodes.push(adjacentNode); + continue; + } + } + } else { + for(const edge of eagle.logicalGraph().getEdges()){ + if(field.getId()===edge.getSrcPortId()){ + const adjacentNode: Node = eagle.logicalGraph().findNodeByKeyQuiet(edge.getDestNodeKey()); + adjacentNodes.push(adjacentNode); + continue; + } + } + } + + return adjacentNodes; + } + + static directionOffset(x: boolean, direction: Eagle.Direction){ + if (x){ + switch (direction){ + case Eagle.Direction.Left: + return -50; + case Eagle.Direction.Right: + return 50; + default: + return 0; + } + } else { + switch (direction){ + case Eagle.Direction.Up: + return -50; + case Eagle.Direction.Down: + return 50; + default: + return 0; + } + } + } + + static calculateConnectionAngle(currentNodePos:any, linkedNodePos:any) : number { + const xDistance = linkedNodePos.x-currentNodePos.x + const yDistance = currentNodePos.y-linkedNodePos.y + const angle = Math.atan2(yDistance, xDistance) + return angle + } + + static calculatePortPos(angle:number, nodeRadius:number, portRadius:number) : {x:number, y:number} { + const newX = nodeRadius+(portRadius*Math.cos(angle)) + const newY = nodeRadius-(portRadius*Math.sin(angle)) + + return {x: newX, y: newY}; + } + + static getCurveDirection(angle:any) : any { + let result + if(angle > Math.PI/4 && angle < 3*Math.PI/4){ + result = Eagle.Direction.Up + }else if(angle < -Math.PI/4 && angle > -3*Math.PI/4){ + result = Eagle.Direction.Down + }else if(angle > -Math.PI/4 && angle < Math.PI/4){ + result = Eagle.Direction.Right + }else{ + result = Eagle.Direction.Left + } + return result + } + + static edgeDirectionAngle(angle: number): number { + const PiOver2: number = Math.PI / 2; + const PiOver4: number = Math.PI / 4; + + // find the nearest compass angle + const nearestCompassAngle = PiOver2 * Math.round(angle / PiOver2); + + // find the difference between the given angle and the nearest compass angle + const diffAngle = Math.abs(angle - nearestCompassAngle); + + // the maximum difference will be PI/4, so get the turn the difference into a ratio between 0 and 1. + const diffRatio = diffAngle / PiOver4; + + // now interpolate between the original angle, and the nearest compass angle, using the ratio calculated above + // this "weights" the interpolation towards the compass directions + const interpolatedAngle = (diffRatio * angle) + ((1-diffRatio) * nearestCompassAngle); + + return interpolatedAngle; + } + + static createBezier(edge:Edge, srcNodeRadius:number, destNodeRadius:number, srcNodePosition: {x: number, y: number}, destNodePosition: {x: number, y: number}, srcField: Field, destField: Field, sourcePortIsInput: boolean) : string { + + //since the svg parent is translated -50% to center our working area, we need to add half of its width to correct the positions + // TODO: remove magic numbers here (5000) + destNodePosition={x:destNodePosition.x+5000,y:destNodePosition.y+5000} + srcNodePosition={x:srcNodePosition.x+5000,y:srcNodePosition.y+5000} + + // determine if the edge falls below a certain length threshold + const edgeLength = Math.sqrt((destNodePosition.x - srcNodePosition.x)**2 + (destNodePosition.y - srcNodePosition.y)**2); + + //determining if the edge's length is below a certain threshhold. if it is we will draw the edge straight and remove the arrow + const isShortEdge: boolean = edgeLength < srcNodeRadius * GraphConfig.SWITCH_TO_STRAIGHT_EDGE_MULTIPLIER; + + if (edge !== null){ + edge.setIsShortEdge(isShortEdge) + } + + // calculate the length from the src and dest nodes at which the control points will be placed + const lengthToControlPoints = edgeLength * 0.4; + + // calculate the angle for the src and dest ports + const srcPortAngle: number = GraphRenderer.calculateConnectionAngle(srcNodePosition, destNodePosition); + const destPortAngle: number = srcPortAngle + Math.PI; + + // calculate the offset for the src and dest ports, based on the angles + let srcPortOffset; + let destPortOffset; + if (srcField){ + if (sourcePortIsInput){ + srcPortOffset = srcField.getInputPosition(); + } else { + srcPortOffset = srcField.getOutputPosition(); + } + } else { + srcPortOffset = GraphRenderer.calculatePortPos(srcPortAngle, srcNodeRadius, srcNodeRadius); + } + if (destField){ + if (sourcePortIsInput){ + destPortOffset = destField.getOutputPosition(); + } else { + destPortOffset = destField.getInputPosition(); + } + } else { + destPortOffset = GraphRenderer.calculatePortPos(destPortAngle, destNodeRadius, destNodeRadius); + } + + // calculate the coordinates of the start and end of the edge + const x1 = srcNodePosition.x + srcPortOffset.x; + const y1 = srcNodePosition.y + srcPortOffset.y; + const x2 = destNodePosition.x + destPortOffset.x; + const y2 = destNodePosition.y + destPortOffset.y; + + // if edge is short, use simplified rendering + if (isShortEdge){ + return "M " + x1 + " " + y1 + " L " + x2 + " " + y2; + } + + // otherwise, calculate an angle for the src and dest control points + const srcCPAngle = GraphRenderer.edgeDirectionAngle(srcPortAngle); + const destCPAngle = GraphRenderer.edgeDirectionAngle(destPortAngle); + + // calculate the offset for the src and dest control points, based on the angles + const srcCPOffset = GraphRenderer.calculatePortPos(srcCPAngle, srcNodeRadius, lengthToControlPoints); + const destCPOffset = GraphRenderer.calculatePortPos(destCPAngle, destNodeRadius, lengthToControlPoints); + + // calculate the coordinates of the two control points + const c1x = srcNodePosition.x + srcCPOffset.x; + const c1y = srcNodePosition.y + srcCPOffset.y; + const c2x = destNodePosition.x + destCPOffset.x; + const c2y = destNodePosition.y + destCPOffset.y; + + + //the edge parameter is null if we are rendering a comment edge and this is not needed + if(edge != null){ + //we are hiding the arrows if the edge is too short + if(edgeLength > GraphConfig.EDGE_DISTANCE_ARROW_VISIBILITY){ + //were adding the position and shape of the arrow to the edges + const arrowPosx = GraphRenderer.getCoordinateOnBezier(0.5,x1,c1x,c2x,x2) + const arrowPosy = GraphRenderer.getCoordinateOnBezier(0.5,y1,c1y,c2y,y2) + + //generating the points for the arrow polygon + const P1x = arrowPosx+GraphConfig.EDGE_ARROW_SIZE + const P1y = arrowPosy + const P2x = arrowPosx-GraphConfig.EDGE_ARROW_SIZE + const P2y = arrowPosy+GraphConfig.EDGE_ARROW_SIZE + const P3x = arrowPosx-GraphConfig.EDGE_ARROW_SIZE + const P3y = arrowPosy-GraphConfig.EDGE_ARROW_SIZE + + //we are calculating the angle the arrow should be pointing by getting two positions on either sider of the center of the bezier curve then calculating the angle + const anglePos1x = GraphRenderer.getCoordinateOnBezier(0.45,x1,c1x,c2x,x2) + const anglePos1y = GraphRenderer.getCoordinateOnBezier(0.45,y1,c1y,c2y,y2) + const anglePos2x = GraphRenderer.getCoordinateOnBezier(0.55,x1,c1x,c2x,x2) + const anglePos2y = GraphRenderer.getCoordinateOnBezier(0.55,y1,c1y,c2y,y2) + + const arrowAngle = GraphRenderer.calculateConnectionAngle({x:anglePos1x,y:anglePos1y}, {x:anglePos2x,y:anglePos2y}) + + $('#'+edge.getId() +" polygon").show() + $('#'+edge.getId() +" polygon").attr('points', P1x +','+P1y+', '+ P2x +','+P2y +', '+ P3x +','+P3y) + // the rotate argument takes three inputs, (angle in deg, x , y coordinates for the midpoint to rotate around) + $('#'+edge.getId() +" polygon").attr({'transform':'rotate('+arrowAngle*(180/Math.PI)*-1+','+arrowPosx+','+arrowPosy +')'}); + }else{ + $('#'+edge.getId() +" polygon").hide() + } + } + + + return "M " + x1 + " " + y1 + " C " + c1x + " " + c1y + ", " + c2x + " " + c2y + ", " + x2 + " " + y2; + // return "M " + x1 + " " + y1 + ", " + x2 + " " + y2; //straighten edges + } + + static getCoordinateOnBezier(t:number,p1:number,p2:number,p3:number,p4:number) : number { + //t is a number from 0-1 that specifies where on the curve we want the coordinates. 0.5 is the center. + return (1-t)*(1-t)*(1-t)*p1 + 3*(1-t)*(1-t)*t*p2 + 3*(1-t)*t*t*p3 + t*t*t*p4; + } + + static getPath(edge: Edge) : string { + const eagle: Eagle = Eagle.getInstance(); + const lg: LogicalGraph = eagle.logicalGraph(); + + const srcNode: Node = lg.findNodeByKeyQuiet(edge.getSrcNodeKey()); + const destNode: Node = lg.findNodeByKeyQuiet(edge.getDestNodeKey()); + if(srcNode===null||destNode===null){ + return '' + } + const srcField: Field = srcNode.findFieldById(edge.getSrcPortId()); + const destField: Field = destNode.findFieldById(edge.getDestPortId()); + + return this._getPath(edge,srcNode, destNode, srcField, destField, eagle); + } + + static getPathComment(commentNode: Node) : string { + const eagle: Eagle = Eagle.getInstance(); + const lg: LogicalGraph = eagle.logicalGraph(); + + const srcNode: Node = commentNode; + const destNode: Node = lg.findNodeByKeyQuiet(commentNode.getSubjectKey()); + + return this._getPath(null,srcNode, destNode, null, null, eagle); + } + + static getPathDraggingEdge : ko.PureComputed = ko.pureComputed(() => { + if (GraphRenderer.portDragSourceNode() === null){ + console.warn('GraphRenderer.getPathDraggingEdge(): no source node detected') + return ''; + } + + const srcNodeRadius: number = GraphRenderer.portDragSourceNode().getRadius(); + const destNodeRadius: number = 0; + const srcX: number = GraphRenderer.portDragSourceNode().getPosition().x - srcNodeRadius; + const srcY: number = GraphRenderer.portDragSourceNode().getPosition().y - srcNodeRadius; + const destX: number = GraphRenderer.mousePosX(); + const destY: number = GraphRenderer.mousePosY(); + + const srcField: Field = GraphRenderer.portDragSourcePort(); + const destField: Field = null; + + return GraphRenderer.createBezier(null, srcNodeRadius, destNodeRadius, {x:srcX, y:srcY}, {x:destX, y:destY}, srcField, destField, GraphRenderer.portDragSourcePortIsInput); + }, this); + + static getPathSuggestedEdge : ko.PureComputed = ko.pureComputed(() => { + if (GraphRenderer.portDragSuggestedNode() === null){ + return ''; + } + + const srcNodeRadius: number = 0; + const destNodeRadius: number = GraphRenderer.portDragSuggestedNode().getRadius(); + const srcX: number = GraphRenderer.mousePosX(); + const srcY: number = GraphRenderer.mousePosY(); + const destX = GraphRenderer.portDragSuggestedNode().getPosition().x - destNodeRadius; + const destY = GraphRenderer.portDragSuggestedNode().getPosition().y - destNodeRadius; + const srcField: Field = null; + const destField: Field = GraphRenderer.portDragSuggestedField(); + + return GraphRenderer.createBezier(null, srcNodeRadius, destNodeRadius, {x:srcX, y:srcY}, {x:destX, y:destY}, srcField, destField, GraphRenderer.portDragSourcePortIsInput); + }, this); + + static _getPath(edge:Edge, srcNode: Node, destNode: Node, srcField: Field, destField: Field, eagle: Eagle) : string { + if (srcNode === null || destNode === null){ + console.warn("Cannot getPath between null nodes. srcNode:", srcNode, "destNode:", destNode); + return ""; + } + + const srcNodeRadius = srcNode.getRadius() + const destNodeRadius = destNode.getRadius() + + // we subtract node radius from all these numbers to account for the transform translate(-50%, -50%) css on the nodes + const srcX = srcNode.getPosition().x -srcNodeRadius; + const srcY = srcNode.getPosition().y -srcNodeRadius; + const destX = destNode.getPosition().x -destNodeRadius; + const destY = destNode.getPosition().y -destNodeRadius; + + return GraphRenderer.createBezier(edge, srcNodeRadius, destNodeRadius,{x:srcX, y:srcY}, {x:destX, y:destY}, srcField, destField, false); + } + + static scrollZoom = (eagle: Eagle, event: JQueryEventObject) : void => { + const e: WheelEvent = event.originalEvent; + + const wheelDelta = e.deltaY; + const zoomDivisor = Setting.findValue(Setting.GRAPH_ZOOM_DIVISOR); + + const xsb = this.SCREEN_TO_GRAPH_POSITION_X(null) + const ysb = this.SCREEN_TO_GRAPH_POSITION_Y(null) + + eagle.globalScale(eagle.globalScale()*(1-(wheelDelta/zoomDivisor))); + + if(eagle.globalScale()<0){ + //prevent negative scale which results in an inverted graph + eagle.globalScale(Math.abs(eagle.globalScale())) + } + + const xsa = this.SCREEN_TO_GRAPH_POSITION_X(null) + const ysa = this.SCREEN_TO_GRAPH_POSITION_Y(null) + + const movex = xsa-xsb + const movey = ysa-ysb + + eagle.globalOffsetX(eagle.globalOffsetX()+movex) + eagle.globalOffsetY(eagle.globalOffsetY()+movey) + } + + static startDrag = (node: Node, event: MouseEvent) : void => { + const eagle = Eagle.getInstance(); + //resetting the shift event + GraphRenderer.dragSelectionHandled(false) + //these two are needed to keep track of these modifiers for the mouse move and release event + GraphRenderer.altSelect = event.altKey + GraphRenderer.shiftSelect = event.shiftKey + + if(node === null || event.which === 2){ + //if no node is selected or we are dragging using middle mouse, we are dragging the background + GraphRenderer.dragSelectionHandled(true) + eagle.isDragging(true); + } else if(!node.isEmbedded()){ + //embedded nodes, aka input and output applications of constructs, cant be dragged + eagle.isDragging(true); + eagle.draggingNode(node); + GraphRenderer.nodeDragElement = event.target + GraphRenderer.nodeDragNode = node + GraphRenderer.dragStartPosition = {x:event.pageX,y:event.pageY} + GraphRenderer.dragCurrentPosition = {x:event.pageX,y:event.pageY} + + if(node.getParentKey() != null){ + const parentNode = eagle.logicalGraph().findNodeByKeyQuiet(node.getParentKey()) + $('#'+parentNode.getId()).removeClass('transition') + GraphRenderer.NodeParentRadiusPreDrag = parentNode.getRadius() + } + } + + //select handlers + if(node !== null && event.which != 2 && !event.shiftKey){ + + // check if shift key is down, if so, add or remove selected node to/from current selection | keycode 2 is the middle mouse button + if (node !== null && event.shiftKey && !event.altKey){ + GraphRenderer.dragSelectionHandled(true) + eagle.editSelection(Eagle.RightWindowMode.Inspector, node, Eagle.FileType.Graph); + } else if(!eagle.objectIsSelected(node)) { + eagle.setSelection(Eagle.RightWindowMode.Inspector, node, Eagle.FileType.Graph); + } + + //check for alt clicking, if so, add the target node and its children to the selection + if(event.altKey&&node.isGroup()||GraphRenderer.dragSelectionDoubleClick&&node.isGroup()){ + GraphRenderer.selectNodeAndChildren(node,this.shiftSelect) + } + }else{ + if(event.shiftKey){ + //drag selection region handler + GraphRenderer.isDraggingSelectionRegion = true + GraphRenderer.selectionRegionStart = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null),y:GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null)} + GraphRenderer.selectionRegionEnd = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null),y:GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null)} + //making the selection box visible + $('#selectionRectangle').show() + + //setting start and end region to current mouse co-ordinates + $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionStart.x+'px','top':GraphRenderer.selectionRegionStart.y+'px'}) + const containerWidth = $('#logicalGraph').width() + const containerHeight = $('#logicalGraph').height() + + //turning the graph coordinates into a distance from bottom/right for css inset before applying + const selectionBottomOffset = containerHeight - GraphRenderer.selectionRegionEnd.y + const selectionRightOffset = containerWidth - GraphRenderer.selectionRegionEnd.x + $('#selectionRectangle').css({'right':selectionRightOffset+'px','bottom':selectionBottomOffset+'px'}) + }else{ + //if node is null, the empty canvas has been clicked. clear the selection + eagle.setSelection(Eagle.RightWindowMode.Inspector, null, Eagle.FileType.Graph); + + } + } + + //this is the timeout for the double click that is used to select the children of constructs + GraphRenderer.dragSelectionDoubleClick = true + setTimeout(function () { + GraphRenderer.dragSelectionDoubleClick = false + }, 200) + } + + static mouseMove = (eagle: Eagle, event: JQueryEventObject) : void => { + const mouseEvent: MouseEvent = event.originalEvent; + GraphRenderer.dragCurrentPosition = {x:event.pageX,y:event.pageY} + if (eagle.isDragging()){ + if (eagle.draggingNode() !== null && !GraphRenderer.isDraggingSelectionRegion ){ + const node:Node = eagle.draggingNode() + $('.node.transition').removeClass('transition') + + // remember node parent from before things change + const oldParent: Node = eagle.logicalGraph().findNodeByKeyQuiet(node.getParentKey()); + + // move node + eagle.selectedObjects().forEach(function(obj){ + if(obj instanceof Node){ + obj.changePosition(mouseEvent.movementX/eagle.globalScale(), mouseEvent.movementY/eagle.globalScale()); + } + }) + + //construct resizing + if(node.getParentKey() != null){ + if(oldParent.getRadius()>GraphRenderer.NodeParentRadiusPreDrag+GraphConfig.CONSTRUCT_DRAG_OUT_DISTANCE){ + // GraphRenderer._updateNodeParent(node, null, false, allowGraphEditing); + + oldParent.setRadius(GraphRenderer.NodeParentRadiusPreDrag) + } + } + + // check for nodes underneath the node we dropped + const parent: Node = eagle.logicalGraph().checkForNodeAt(node.getPosition().x, node.getPosition().y, node.getRadius(), node.getKey(), true); + + // check if new candidate parent is already a descendent of the node, this would cause a circular hierarchy which would be bad + const ancestorOfParent = GraphRenderer.isAncestor(parent, node); + + // keep track of whether we would update any node parents + const updated = {parent: false}; + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING); + + // if a parent was found, update + if (parent !== null && node.getParentKey() !== parent.getKey() && node.getKey() !== parent.getKey() && !ancestorOfParent && !node.isEmbedded()){ + GraphRenderer._updateNodeParent(node, parent.getKey(), updated, allowGraphEditing); + } + + // if no parent found, update + if (parent === null && node.getParentKey() !== null && !node.isEmbedded()){ + GraphRenderer._updateNodeParent(node, null, updated, allowGraphEditing); + } + if (oldParent !== null){ + // moved out of a construct + $('#'+oldParent.getId()).addClass('transition') + } + // recalculate size of parent (or oldParent) + if (parent === null){ + + } else { + // moved into or within a construct + $('#'+parent.getId()).removeClass('transition') + } + + } else if(GraphRenderer.isDraggingSelectionRegion){ + GraphRenderer.selectionRegionEnd = {x:GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null), y:this.SCREEN_TO_GRAPH_POSITION_Y(null)} + const containerWidth = $('#logicalGraph').width() + const containerHeight = $('#logicalGraph').height() + + if(GraphRenderer.selectionRegionEnd.x>GraphRenderer.selectionRegionStart.x){ + $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionStart.x+'px','right':containerWidth - GraphRenderer.selectionRegionEnd.x+'px'}) + }else{ + $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionEnd.x+'px','right':containerWidth - GraphRenderer.selectionRegionStart.x+'px'}) + } + + if(GraphRenderer.selectionRegionEnd.y>GraphRenderer.selectionRegionStart.y){ + $('#selectionRectangle').css({'top':GraphRenderer.selectionRegionStart.y+'px','bottom':containerHeight - GraphRenderer.selectionRegionEnd.y+'px'}) + }else{ + $('#selectionRectangle').css({'top':GraphRenderer.selectionRegionEnd.y+'px','bottom':containerHeight - GraphRenderer.selectionRegionStart.y+'px'}) + } + + }else{ + // move background + eagle.globalOffsetX(eagle.globalOffsetX() + mouseEvent.movementX/eagle.globalScale()); + eagle.globalOffsetY(eagle.globalOffsetY() + mouseEvent.movementY/eagle.globalScale()); + } + } + + if(GraphRenderer.draggingPort){ + GraphRenderer.portDragging(event) + } + + } + + static endDrag = (node: Node) : void => { + const eagle = Eagle.getInstance(); + + // if we dragged a selection region + if (GraphRenderer.isDraggingSelectionRegion){ + const nodes: Node[] = GraphRenderer.findNodesInRegion(GraphRenderer.selectionRegionStart.x, GraphRenderer.selectionRegionEnd.x, GraphRenderer.selectionRegionStart.y, GraphRenderer.selectionRegionEnd.y); + + //checking if there was no drag distance, if so we are clicking a single object and we will toggle its seletion + if(Math.abs(GraphRenderer.selectionRegionStart.x-GraphRenderer.selectionRegionEnd.x)+Math.abs(GraphRenderer.selectionRegionStart.y - GraphRenderer.selectionRegionEnd.y)<3){ + if(GraphRenderer.altSelect){ + GraphRenderer.selectNodeAndChildren(node,this.shiftSelect) + } + eagle.editSelection(Eagle.RightWindowMode.Inspector, node,Eagle.FileType.Graph); + }else{ + const edges: Edge[] = GraphRenderer.findEdgesContainedByNodes(eagle.logicalGraph().getEdges(), nodes); + const objects: (Node | Edge)[] = []; + + // only add those objects which are not already selected + for (const node of nodes){ + if (!eagle.objectIsSelected(node)){ + objects.push(node); + } + } + for (const edge of edges){ + if (!eagle.objectIsSelected(edge)){ + objects.push(edge); + } + } + + objects.forEach(function(element){ + eagle.editSelection(Eagle.RightWindowMode.Hierarchy, element, Eagle.FileType.Graph ) + }) + } + + GraphRenderer.selectionRegionStart.x = 0; + GraphRenderer.selectionRegionStart.y = 0; + GraphRenderer.selectionRegionEnd.x = 0; + GraphRenderer.selectionRegionEnd.y = 0; + + // finish selecting a region + GraphRenderer.isDraggingSelectionRegion = false; + + //hide the selection rectangle + $('#selectionRectangle').hide() + + // necessary to make un-collapsed nodes show up + eagle.logicalGraph.valueHasMutated(); + } + + // if we dragged a node + if (!GraphRenderer.isDraggingSelectionRegion){ + // check if moving whole graph, or just a single node + if (node !== null){ + eagle.undo().pushSnapshot(eagle, "Move '" + node.getName() + "' node"); + } + } + + GraphRenderer.dragSelectionHandled(true) + eagle.isDragging(false); + eagle.draggingNode(null) + + } + + static findNodesInRegion(left: number, right: number, top: number, bottom: number): Node[] { + const eagle = Eagle.getInstance(); + const result: Node[] = []; + const nodeData : Node[] = GraphRenderer.depthFirstTraversalOfNodes(eagle.logicalGraph(), eagle.showDataNodes()); + + // re-assign left, right, top, bottom in case selection region was not dragged in the typical NW->SE direction + const realLeft = left <= right ? left : right; + const realRight = left <= right ? right : left; + const realTop = top <= bottom ? top : bottom; + const realBottom = top <= bottom ? bottom : top; + + for (let i = nodeData.length - 1; i >= 0 ; i--){ + const node : Node = nodeData[i]; + + // use center of node as position + const centerX : number = node.getPosition().x + const centerY : number = node.getPosition().y + const nodeRadius : number = node.getRadius() + + //checking if the node is fully inside the selection box + if (centerX+-nodeRadius >= realLeft && realRight+-nodeRadius >= centerX && centerY+-nodeRadius >= realTop && realBottom+-nodeRadius >= centerY){ + result.push(node); + } + } + + return result; + } + + static selectNodeAndChildren(node:Node,addative:boolean) : void { + const eagle = Eagle.getInstance(); + GraphRenderer.dragSelectionHandled(true) + //if shift is not clicked, we first clear the selection + if(!addative){ + eagle.setSelection(Eagle.RightWindowMode.Inspector, null, Eagle.FileType.Graph); + eagle.editSelection(Eagle.RightWindowMode.Inspector, node, Eagle.FileType.Graph); + } + + //getting all children, including children of child constructs etc.. + let childIsConstruct = true + const constructs : Node[] = [node]; + + while(childIsConstruct){ + let constructFound = false + let i = -1 + constructs.forEach(function(construct){ + i++ + eagle.logicalGraph().getNodes().forEach(function(obj){ + if(obj.getParentKey()===construct.getKey()){ + eagle.editSelection(Eagle.RightWindowMode.Inspector, obj, Eagle.FileType.Graph); + + if(obj.isGroup()){ + constructFound = true + constructs.push(obj) + } + } + }) + constructs.splice(i,1) + }) + if(!constructFound){ + childIsConstruct = false + } + } + } + + static getEdges(graph: LogicalGraph, showDataNodes: boolean): Edge[]{ + if (showDataNodes){ + return graph.getEdges(); + } else { + //return [graph.getEdges()[0]]; + const edges: Edge[] = []; + + for (const edge of graph.getEdges()){ + let srcHasConnectedInput: boolean = false; + let destHasConnectedOutput: boolean = false; + + for (const e of graph.getEdges()){ + if (e.getDestNodeKey() === edge.getSrcNodeKey()){ + srcHasConnectedInput = true; + } + if (e.getSrcNodeKey() === edge.getDestNodeKey()){ + destHasConnectedOutput = true; + } + } + + const srcIsDataNode: boolean = GraphRenderer.findNodeWithKey(edge.getSrcNodeKey(), graph.getNodes()).isData(); + const destIsDataNode: boolean = GraphRenderer.findNodeWithKey(edge.getDestNodeKey(), graph.getNodes()).isData(); + //console.log("edge", edge.getId(), "srcIsDataNode", srcIsDataNode, "srcHasConnectedInput", srcHasConnectedInput, "destIsDataNode", destIsDataNode, "destHasConnectedOutput", destHasConnectedOutput); + + if (destIsDataNode){ + if (!destHasConnectedOutput){ + // draw edge as normal + edges.push(edge); + } + continue; + } + + if (srcIsDataNode){ + if (srcHasConnectedInput){ + // build a new edge + const newSrc = GraphRenderer.findInputToDataNode(graph.getEdges(), edge.getSrcNodeKey()); + edges.push(new Edge(newSrc.nodeKey, newSrc.portId, edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false)); + } else { + // draw edge as normal + edges.push(edge); + } + } + } + + return edges; + } + } + + static findEdgesContainedByNodes(edges: Edge[], nodes: Node[]): Edge[]{ + const result: Edge[] = []; + + for (const edge of edges){ + const srcKey = edge.getSrcNodeKey(); + const destKey = edge.getDestNodeKey(); + let srcFound = false; + let destFound = false; + + for (const node of nodes){ + if ((node.getKey() === srcKey) || + (node.hasInputApplication() && node.getInputApplication().getKey() === srcKey) || + (node.hasOutputApplication() && node.getOutputApplication().getKey() === srcKey)){ + srcFound = true; + } + + if ((node.getKey() === destKey) || + (node.hasInputApplication() && node.getInputApplication().getKey() === destKey) || + (node.hasOutputApplication() && node.getOutputApplication().getKey() === destKey)){ + destFound = true; + } + } + + if (srcFound && destFound){ + result.push(edge); + } + } + + return result; + } + + + static findInputToDataNode(edges: Edge[], nodeKey: number) : {nodeKey:number, portId: string}{ + for (const edge of edges){ + if (edge.getDestNodeKey() === nodeKey){ + return { + nodeKey: edge.getSrcNodeKey(), + portId: edge.getSrcPortId() + }; + } + } + + return null; + } + + static centerConstructs = (construct:Node, graphNodes:Node[]) :void => { + const constructsList : Node[]=[] + if(construct === null){ + graphNodes.forEach(function(node){ + if(node.isGroup()){ + constructsList.push(node) + } + }) + } + let findConstructId + const orderedConstructList:Node[] = [] + + constructsList.forEach(function(x){ + if(x.getParentKey()===null){ + let finished = false // while there are child construct found in this construct nest group + + findConstructId = x.getKey() + orderedConstructList.unshift(x) + while(!finished){ + let found = false + for(const entry of constructsList){ + if(entry.getParentKey() === findConstructId){ + orderedConstructList.unshift(entry) + findConstructId = entry.getKey() + found = true + } + } + if(!found){ + finished = true + } + } + } + }) + + orderedConstructList.forEach(function(constr){ + GraphRenderer.centerConstruct(constr,graphNodes) + }) + } + + static centerConstruct = (construct:Node,graphNodes:Node[]) : void => { + let childCount = 0 + + let minX : number = Number.MAX_VALUE; + let minY : number = Number.MAX_VALUE; + let maxX : number = -Number.MAX_VALUE; + let maxY : number = -Number.MAX_VALUE; + for (const node of graphNodes){ + + if (!node.isEmbedded() && node.getParentKey() === construct.getKey()){ + childCount++ + if (node.getPosition().x - node.getRadius() < minX){ + minX = node.getPosition().x - node.getRadius(); + } + if (node.getPosition().y - node.getRadius() < minY){ + minY = node.getPosition().y - node.getRadius(); + } + if (node.getPosition().x + node.getRadius() > maxX){ + maxX = node.getPosition().x + node.getRadius(); + } + if (node.getPosition().y + node.getRadius() > maxY){ + maxY = node.getPosition().y + node.getRadius(); + } + } + } + + if(childCount === 0){ + return + } + + // determine the centroid of the contruct + const centroidX = minX + ((maxX - minX) / 2); + const centroidY = minY + ((maxY - minY) / 2); + + construct.setPosition(centroidX,centroidY) + GraphRenderer.resizeConstruct(construct) + } + + static translateLegacyGraph = () : void =>{ + const eagle = Eagle.getInstance(); + //we are moving each node by half its radius to counter the fact that the new graph renderer treats the node's visual center as node position, previously the node position was in its top left. + if(GraphRenderer.legacyGraph){ + //we need to calculate the construct radius in relation to it's children + eagle.logicalGraph().getNodes().forEach(function(node){ + if(!node.isGroup()&&!node.isEmbedded()){ + node.setPosition(node.getPosition().x+node.getRadius()/2,node.getPosition().y + node.getRadius()/2,false) + } + }) + GraphRenderer.centerConstructs(null,eagle.logicalGraph().getNodes()) + } + GraphRenderer.legacyGraph = false + } + + static moveChildNodes = (node: Node, deltax : number, deltay : number) : void => { + const eagle = Eagle.getInstance(); + + // get id of parent nodeIndex + const parentKey : number = node.getKey(); + + // loop through all nodes, if they belong to the parent's group, move them too + for (let i = 0 ; i < eagle.logicalGraph().getNodes().length ; i++){ + const node = eagle.logicalGraph().getNodes()[i]; + if (node.getParentKey() === parentKey){ + node.changePosition(deltax, deltay); + GraphRenderer.moveChildNodes(node, deltax, deltay); + } + } + } + + static isAncestor = (node : Node, possibleAncestor : Node) : boolean => { + const eagle = Eagle.getInstance(); + let n : Node = node; + let iterations = 0; + + if (n === null){ + return false; + } + + while (true){ + if (iterations > 32){ + console.error("too many iterations in isDescendent()"); + return null; + } + + iterations += 1; + + // check if found + if (n.getKey() === possibleAncestor.getKey()){ + return true; + } + + // otherwise keep traversing upwards + const newKey = n.getParentKey(); + + // if we reach a null parent, we are done looking + if (newKey === null){ + return false; + } + + //n = findNodeWithKey(newKey, nodeData); + n = eagle.logicalGraph().findNodeByKey(newKey); + } + } + + // update the parent of the given node + // however, if allGraphEditing is false, then don't update + // always keep track of whether an update would have happened, sp we can warn user + static _updateNodeParent = (node: Node, parentKey: number, updated: {parent: boolean}, allowGraphEditing: boolean): void => { + if (node.getParentKey() !== parentKey){ + if (allowGraphEditing){ + node.setParentKey(parentKey); + } + updated.parent = true; + } + } + + // resize a construct so that it contains its children + // NOTE: does not move the construct + static resizeConstruct = (construct: Node, allowMovement: boolean = false): void => { + const eagle = Eagle.getInstance(); + let maxDistance = 0; + + // loop through all children - find distance from center of construct + for (const node of eagle.logicalGraph().getNodes()){ + if (node.getParentKey() === construct.getKey()){ + const dx = construct.getPosition().x - node.getPosition().x; + const dy = construct.getPosition().y - node.getPosition().y; + const distance = Math.sqrt(dx*dx + dy*dy); + //console.log("distance to", node.getName(), distance); + + const paddedDistance = distance + node.getRadius() + GraphConfig.CONSTRUCT_MARGIN; + //console.log("paddedDistance to", node.getName(), paddedDistance, "(", node.getRadius(), ")"); + + maxDistance = Math.max(maxDistance, paddedDistance); + } + } + + // make sure constructs are never below minimum size + maxDistance = Math.max(maxDistance, GraphConfig.MINIMUM_CONSTRUCT_RADIUS); + + //console.log("Resize", construct.getName(), "radius to", maxDistance, "to contain", numChildren, "children"); + if(construct.isGroup()){ + construct.setRadius(maxDistance); + } + } + + static updateMousePos = (): void => { + // grab and convert mouse position to graph coordinates + const divOffset = $('#logicalGraph').offset(); + const mouseX = (event).pageX - divOffset.left; + const mouseY = (event).pageY - divOffset.top; + GraphRenderer.mousePosX(GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null)); + GraphRenderer.mousePosY(GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null)); + } + + static portDragStart = (port:Field, usage:string) : void => { + const eagle = Eagle.getInstance(); + + GraphRenderer.updateMousePos(); + + //prevents moving the node when dragging the port + event.stopPropagation(); + + //preparing necessary port info + GraphRenderer.draggingPort = true + GraphRenderer.portDragSourceNode(eagle.logicalGraph().findNodeByKey(port.getNodeKey())); + GraphRenderer.portDragSourcePort(port); + GraphRenderer.portDragSourcePortIsInput = usage === 'input'; + GraphRenderer.renderDraggingPortEdge(true); + + //setting up the port event listeners + $('#logicalGraphParent').on('mouseup.portDrag',function(){GraphRenderer.portDragEnd()}) + $('.node .body').on('mouseup.portDrag',function(){GraphRenderer.portDragEnd()}) + + + // check for nearby nodes + const matchingNodes = GraphRenderer.findMatchingNodes(GraphRenderer.portDragSourceNode().getKey()); + + // check for nearest matching port in the nearby nodes + const matchingPorts = GraphRenderer.findMatchingPorts(GraphRenderer.mousePosX(), GraphRenderer.mousePosY(), matchingNodes, GraphRenderer.portDragSourceNode(), GraphRenderer.portDragSourcePort(), GraphRenderer.portDragSourcePortIsInput); + GraphRenderer.matchingPortList = matchingPorts + } + + static portDragging = (event:any) : void => { + GraphRenderer.updateMousePos(); + + // check for nearest matching port in the nearby nodes + const match: {node: Node, field: Field} = GraphRenderer.findNearestMatchingPort(GraphRenderer.mousePosX(), GraphRenderer.mousePosY(), GraphRenderer.portDragSourceNode(), GraphRenderer.portDragSourcePort(), GraphRenderer.portDragSourcePortIsInput); + + if (match.field !== null){ + GraphRenderer.portDragSuggestedNode(match.node); + GraphRenderer.portDragSuggestedField(match.field); + } else { + GraphRenderer.portDragSuggestedNode(null); + GraphRenderer.portDragSuggestedField(null); + } + } + + static portDragEnd = () : void => { + const eagle = Eagle.getInstance(); + + GraphRenderer.draggingPort = false; + // cleaning up the port drag event listeners + $('#logicalGraphParent').off('mouseup.portDrag') + $('.node .body').off('mouseup.portDrag') + + if ((GraphRenderer.destinationPort !== null || GraphRenderer.portDragSuggestedField() !== null) && GraphRenderer.portMatchCloseEnough()){ + const srcNode: Node = GraphRenderer.portDragSourceNode(); + const srcPort: Field = GraphRenderer.portDragSourcePort(); + + let destNode: Node = null; + let destPort: Field = null; + + if (GraphRenderer.destinationPort !== null){ + destNode = GraphRenderer.destinationNode; + destPort = GraphRenderer.destinationPort; + } else { + destNode = GraphRenderer.portDragSuggestedNode(); + destPort = GraphRenderer.portDragSuggestedField(); + } + + this.createEdge(srcNode, srcPort, destNode, destPort); + + // we can stop rendering the dragging edge + GraphRenderer.renderDraggingPortEdge(false); + GraphRenderer.clearEdgeVars(); + } else { + if (GraphRenderer.destinationPort === null){ + this.showUserNodeSelectionContextMenu(); + } else { + // connect to destination port + const srcNode: Node = GraphRenderer.portDragSourceNode(); + const srcPort: Field = GraphRenderer.portDragSourcePort(); + const destNode: Node = GraphRenderer.destinationNode; + const destPort: Field = GraphRenderer.destinationPort; + + this.createEdge(srcNode, srcPort, destNode, destPort); + + // we can stop rendering the dragging edge + GraphRenderer.renderDraggingPortEdge(false); + GraphRenderer.clearEdgeVars(); + } + } + + //resetting some global cached variables + GraphRenderer.matchingPortList.forEach(function(x){ + x.field.setPeek(false) + }) + + GraphRenderer.matchingPortList = [] + eagle.logicalGraph.valueHasMutated(); + } + + static createEdge(srcNode: Node, srcPort: Field, destNode: Node, destPort: Field){ + const eagle = Eagle.getInstance(); + + // check if edge is back-to-front (input-to-output), if so, swap the source and destination + //const backToFront : boolean = (srcPortType === "input" || srcPortType === "outputLocal") && (destPortType === "output" || destPortType === "inputLocal"); + const backToFront : boolean = GraphRenderer.portDragSourcePortIsInput; + const realSourceNode: Node = backToFront ? destNode : srcNode; + const realSourcePort: Field = backToFront ? destPort : srcPort; + const realDestinationNode: Node = backToFront ? srcNode : destNode; + const realDestinationPort: Field = backToFront ? srcPort : destPort; + + // notify user + if (backToFront){ + Utils.showNotification("Automatically reversed edge direction", "The edge began at an input port and ended at an output port, so the direction was reversed.", "info"); + } + + // check if link is valid + const linkValid : Eagle.LinkValid = Edge.isValid(eagle, null, realSourceNode.getKey(), realSourcePort.getId(), realDestinationNode.getKey(), realDestinationPort.getId(), realSourcePort.getType(), false, false, true, true, {errors:[], warnings:[]}); + + // abort if edge is invalid + if ((Setting.findValue(Setting.ALLOW_INVALID_EDGES) && linkValid === Eagle.LinkValid.Invalid) || linkValid === Eagle.LinkValid.Valid || linkValid === Eagle.LinkValid.Warning){ + if (linkValid === Eagle.LinkValid.Warning){ + GraphRenderer.addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, true, false); + } else { + GraphRenderer.addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, false, false); + } + } else { + console.warn("link not valid, result", linkValid); + } + } + + static showUserNodeSelectionContextMenu(){ + const eagle: Eagle = Eagle.getInstance(); + + //hiding the suggested node edge while the right click menu shows up + GraphRenderer.portDragSuggestedNode(null) + GraphRenderer.portDragSuggestedField(null) + // no destination, ask user to choose a new node + const dataEligible: boolean = GraphRenderer.portDragSourceNode().getCategoryType() !== Category.Type.Data; + + // check if source port is a 'dummy' port + // if so, consider all components as eligible, to ease the creation of new graphs + const sourcePortIsDummy: boolean = GraphRenderer.portDragSourcePort().getDisplayText() === Daliuge.FieldName.DUMMY; + + let eligibleComponents: Node[]; + + if (!sourcePortIsDummy && Setting.findValue(Setting.FILTER_NODE_SUGGESTIONS)){ + // getting matches from both the graph and the palettes list + eligibleComponents = Utils.getComponentsWithMatchingPort('palette graph', !GraphRenderer.portDragSourcePortIsInput, GraphRenderer.portDragSourcePort().getType(), dataEligible); + } else { + // get all nodes with at least one port with opposite "direction" (input/output) from the source node + eligibleComponents = []; + + eagle.palettes().forEach(function(palette){ + palette.getNodes().forEach(function(node){ + if (GraphRenderer.portDragSourcePortIsInput){ + if (node.getOutputPorts().length > 0){ + eligibleComponents.push(node); + } + } else { + if (node.getInputPorts().length > 0){ + eligibleComponents.push(node); + } + } + }) + }); + + eagle.logicalGraph().getNodes().forEach(function(graphNode){ + if (GraphRenderer.portDragSourcePortIsInput){ + if (graphNode.getOutputPorts().length > 0){ + eligibleComponents.push(graphNode); + } + } else { + if (graphNode.getInputPorts().length > 0){ + eligibleComponents.push(graphNode); + } + } + }) + } + + // check we found at least one eligible component + if (eligibleComponents.length === 0){ + Utils.showNotification("Not Found", "No eligible components found for connection to port of this type (" + GraphRenderer.portDragSourcePort().getType() + ")", "info"); + + // stop rendering the dragging edge + GraphRenderer.renderDraggingPortEdge(false); + } else { + + // get list of strings from list of eligible components + const eligibleComponentNames : Node[] = []; + for (const c of eligibleComponents){ + eligibleComponentNames.push(c); + } + + // NOTE: create copy in right click ts because we are using the right click menus to handle the node selection + RightClick.edgeDropSrcNode = GraphRenderer.portDragSourceNode(); + RightClick.edgeDropSrcPort = GraphRenderer.portDragSourcePort(); + RightClick.edgeDropSrcIsInput = GraphRenderer.portDragSourcePortIsInput; + + Eagle.selectedRightClickPosition = {x:GraphRenderer.mousePosX(), y:GraphRenderer.mousePosY()}; + + RightClick.edgeDropCreateNode(eligibleComponentNames, null) + } + } + + static showPort(node: Node, field: Field) :boolean { + const eagle = Eagle.getInstance(); + if(!GraphRenderer.dragSelectionHandled()){ + return false + }else if(node.isPeek()){ + return true + }else if(eagle.objectIsSelected(node)){ + return true + }else if(field.isPeek()){ + return true + }else{ + return false + } + } + + static SCREEN_TO_GRAPH_POSITION_X(x:number) : number { + const eagle = Eagle.getInstance(); + if(x===null){ + x = GraphRenderer.dragCurrentPosition.x + } + return x/eagle.globalScale() - eagle.globalOffsetX(); + } + + static SCREEN_TO_GRAPH_POSITION_Y(y:number) : number { + const eagle = Eagle.getInstance(); + if(y===null){ + y = GraphRenderer.dragCurrentPosition.y + } + return (y-83.77)/eagle.globalScale() -eagle.globalOffsetY(); + } + + static SCREEN_TO_GRAPH_SCALE(n: number) : number { + const eagle = Eagle.getInstance(); + return n * eagle.globalScale(); + } + + static GRAPH_TO_SCREEN_POSITION_X(x: number) : number { + const eagle = Eagle.getInstance(); + // return (x * eagle.globalScale()) + eagle.globalOffsetX() ; + return(x + eagle.globalOffsetX()) * eagle.globalScale() + // return (x + eagle.globalOffsetX())/eagle.globalScale(); + } + + static GRAPH_TO_SCREEN_POSITION_Y(y: number) : number { + const eagle = Eagle.getInstance(); + // return (y * eagle.globalScale()) + eagle.globalOffsetY(); + // return (y + eagle.globalOffsetY())/eagle.globalScale(); + return (y+eagle.globalOffsetY())*eagle.globalScale()+83.77 + } + + static findMatchingNodes(sourceNodeKey: number): Node[]{ + const result: Node[] = []; + const nodeData : Node[] = GraphRenderer.nodeData + + //console.log("findNodesInRange(): sourceNodeKey", sourceNodeKey); + + for (let i = 0; i < nodeData.length; i++){ + // skip the source node + if (nodeData[i].getKey() === sourceNodeKey){ + continue; + } + + // fetch categoryData for the node + const categoryData = CategoryData.getCategoryData(nodeData[i].getCategory()); + let possibleInputs = categoryData.maxInputs; + let possibleOutputs = categoryData.maxOutputs; + + // add categoryData for embedded apps (if they exist) + if (nodeData[i].hasInputApplication()){ + const inputApp = nodeData[i].getInputApplication(); + const inputAppCategoryData = CategoryData.getCategoryData(inputApp.getCategory()); + possibleInputs += inputAppCategoryData.maxInputs; + possibleOutputs += inputAppCategoryData.maxOutputs; + } + if (nodeData[i].hasOutputApplication()){ + const outputApp = nodeData[i].getOutputApplication(); + const outputAppCategoryData = CategoryData.getCategoryData(outputApp.getCategory()); + possibleInputs += outputAppCategoryData.maxInputs; + possibleOutputs += outputAppCategoryData.maxOutputs; + } + + // skip nodes that can't have inputs or outputs + if (possibleInputs === 0 && possibleOutputs === 0){ + continue; + } + + // determine distance from position to this node + // const distance = Utils.positionToNodeDistance(positionX, positionY, nodeData[i]); + + // if (distance <= range){ + //console.log("distance to", nodeData[i].getName(), nodeData[i].getKey(), "=", distance); + result.push(nodeData[i]); + // } + } + + return result; + } + + static depthFirstTraversalOfNodes(graph: LogicalGraph, showDataNodes: boolean) : Node[] { + const indexPlusDepths : {index:number, depth:number}[] = []; + const result : Node[] = []; + + // populate key plus depths + for (let i = 0 ; i < graph.getNodes().length ; i++){ + let nodeHasConnectedInput: boolean = false; + let nodeHasConnectedOutput: boolean = false; + const node = graph.getNodes()[i]; + + // check if node has connected input and output + for (const edge of graph.getEdges()){ + if (edge.getDestNodeKey() === node.getKey()){ + nodeHasConnectedInput = true; + } + + if (edge.getSrcNodeKey() === node.getKey()){ + nodeHasConnectedOutput = true; + } + } + + // skip data nodes, if showDataNodes is false + if (!showDataNodes && node.isData() && nodeHasConnectedInput && nodeHasConnectedOutput){ + continue; + } + + const depth = GraphRenderer.findDepthOfNode(i, graph.getNodes()); + + indexPlusDepths.push({index:i, depth:depth}); + } + + // sort nodes in depth ascending + indexPlusDepths.sort(function(a, b){ + return a.depth - b.depth; + }); + + // write nodes to result in sorted order + for (const indexPlusDepth of indexPlusDepths){ + result.push(graph.getNodes()[indexPlusDepth.index]); + } + + return result; + } + + static findDepthOfNode(index: number, nodes : Node[]) : number { + const eagle = Eagle.getInstance(); + if (index >= nodes.length){ + console.warn("findDepthOfNode() with node index outside range of nodes. index:", index, "nodes.length", nodes.length); + return 0; + } + + let depth : number = 0; + let node : Node = nodes[index]; + let nodeKey : number; + let nodeParentKey : number = node.getParentKey(); + let iterations = 0; + + // follow the chain of parents + while (nodeParentKey != null){ + if (iterations > 10){ + console.error("too many iterations in findDepthOfNode()"); + break; + } + + iterations += 1; + depth += 1; + depth += node.getDrawOrderHint() / 10; + nodeKey = node.getKey(); + nodeParentKey = node.getParentKey(); + + if (nodeParentKey === null){ + return depth; + } + + node = GraphRenderer.findNodeWithKey(nodeParentKey, nodes); + + if (node === null){ + console.error("Node", nodeKey, "has parentKey", nodeParentKey, "but call to findNodeWithKey(", nodeParentKey, ") returned null"); + return depth; + } + + // if parent is selected, add more depth, so that it will appear on top + if (eagle.objectIsSelected(node)){ + depth += 10; + } + } + + depth += node.getDrawOrderHint() / 10; + + // if node is selected, add more depth, so that it will appear on top + if (eagle.objectIsSelected(node)){ + depth += 10; + } + + return depth; + } + + static findNodeWithKey(key: number, nodes: Node[]) : Node { + if (key === null){ + return null; + } + + for (const node of nodes){ + if (node.getKey() === key){ + return node; + } + + // check if the node's inputApp has a matching key + if (node.hasInputApplication()){ + if (node.getInputApplication().getKey() === key){ + return node.getInputApplication(); + } + } + + // check if the node's outputApp has a matching key + if (node.hasOutputApplication()){ + if (node.getOutputApplication().getKey() === key){ + return node.getOutputApplication(); + } + } + } + + console.warn("Cannot find node with key", key); + return null; + } + + + static findMatchingPorts(positionX: number, positionY: number, nearbyNodes: Node[], sourceNode: Node, sourcePort: Field, sourcePortIsInput: boolean) : {node: Node, field: Field}[] { + //console.log("findNearestMatchingPort(), sourcePortIsInput", sourcePortIsInput); + const eagle = Eagle.getInstance(); + const result :{field:Field, node:Node}[]= [] + for (const node of nearbyNodes){ + let portList: Field[] = []; + + // if source node is Data, then no nearby Data nodes can have matching ports + if (sourceNode.getCategoryType() === Category.Type.Data && node.getCategoryType() === Category.Type.Data){ + continue; + } + + // if sourcePortIsInput, we should search for output ports, and vice versa + if (sourcePortIsInput){ + portList = portList.concat(node.getOutputPorts()); + } else { + portList = portList.concat(node.getInputPorts()); + } + + // get inputApplication ports + if (sourcePortIsInput){ + portList = portList.concat(node.getInputApplicationOutputPorts()); + } else { + portList = portList.concat(node.getInputApplicationInputPorts()); + } + + // get outputApplication ports + if (sourcePortIsInput){ + portList = portList.concat(node.getOutputApplicationOutputPorts()); + } else { + portList = portList.concat(node.getOutputApplicationInputPorts()); + } + + for (const port of portList){ + if (!Utils.portsMatch(port, sourcePort)){ + continue; + } + + // if port has no id (broken) then don't consider it as a auto-complete target + if (port.getId() === ""){ + continue; + } + + //this is needed for embedded apps, as the node variable is still the construct + const realNode = eagle.logicalGraph().findNodeByKeyQuiet(port.getNodeKey()) + + result.push({field:port,node:realNode}) + port.setPeek(true) + } + } + + return result + } + + static findNearestMatchingPort(positionX: number, positionY: number, sourceNode: Node, sourcePort: Field, sourcePortIsInput: boolean) : {node: Node, field: Field} { + let minDistance: number = Number.MAX_SAFE_INTEGER; + let minNode: Node = null; + let minPort: Field = null; + GraphRenderer.portMatchCloseEnough(false) + + const portList = GraphRenderer.matchingPortList + for (const x of portList){ + const port = x.field + const node = x.node + + // get position of port + let portX + let portY + if (sourcePortIsInput){ + portX = port.getOutputPosition().x; + portY = port.getOutputPosition().y; + } else { + portX = port.getInputPosition().x; + portY = port.getInputPosition().y; + } + portX = node.getPosition().x + portX + portY = node.getPosition().y + portY + + // get distance to port + const distance = Math.sqrt( Math.pow(portX - positionX, 2) + Math.pow(portY - positionY, 2) ); + + if(distance > GraphConfig.NODE_SUGGESTION_RADIUS){ + continue + } + + // remember this port if it the best so far + if (distance < minDistance){ + minPort = port; + minNode = node; + minDistance = distance; + } + } + if (minDistance = ko.pureComputed(() => { + switch (GraphRenderer.isDraggingPortValid()){ + case Eagle.LinkValid.Unknown: + return "black"; + case Eagle.LinkValid.Impossible: + case Eagle.LinkValid.Invalid: + return GraphConfig.getColor("edgeInvalid"); + case Eagle.LinkValid.Warning: + return GraphConfig.getColor("edgeWarning"); + case Eagle.LinkValid.Valid: + return GraphConfig.getColor("edgeValid"); + } + }, this); + + static suggestedEdgeGetStrokeColor() : string { + if(GraphRenderer.portMatchCloseEnough()){ + return GraphConfig.getColor("edgeAutoComplete"); + }else{ + return GraphConfig.getColor("edgeAutoCompleteSuggestion"); + } + } + + static draggingEdgeGetStrokeType() : string { + return ''; + } + + static suggestedEdgeGetStrokeType() : string { + return ''; + } + + static addEdge(srcNode: Node, srcPort: Field, destNode: Node, destPort: Field, loopAware: boolean, closesLoop: boolean) : void { + const eagle = Eagle.getInstance(); + if (srcPort.getId() === destPort.getId()){ + console.warn("Abort addLink() from port to itself!"); + return; + } + + eagle.addEdge(srcNode, srcPort, destNode, destPort, loopAware, closesLoop, (edge : Edge) : void => { + eagle.checkGraph(); + eagle.logicalGraph.valueHasMutated(); + GraphRenderer.clearEdgeVars(); + }); + } + + static clearEdgeVars(){ + GraphRenderer.portDragSourcePort(null) + GraphRenderer.portDragSourceNode(null) + GraphRenderer.portDragSourcePortIsInput = false + GraphRenderer.destinationPort = null + GraphRenderer.destinationNode = null + GraphRenderer.portDragSuggestedNode(null) + GraphRenderer.portDragSuggestedField(null) + } + + static selectEdge(edge : Edge,event:any){ + const eagle = Eagle.getInstance(); + if (edge !== null){ + if (event.shiftKey){ + eagle.editSelection(Eagle.RightWindowMode.Inspector, edge, Eagle.FileType.Graph); + } else { + eagle.setSelection(Eagle.RightWindowMode.Inspector, edge, Eagle.FileType.Graph); + } + } + } + + static clearPortPeek() : void { + const eagle = Eagle.getInstance(); + eagle.logicalGraph().getNodes().forEach(function(node){ + if(node.isConstruct()){ + if(node.getInputApplication() != null){ + node.getInputApplication().getFields().forEach(function(inputAppField){ + inputAppField.setPeek(false) + }) + } + if(node.getOutputApplication() != null){ + node.getOutputApplication().getFields().forEach(function(outputAppField){ + outputAppField.setPeek(false) + }) + } + } + + node.getFields().forEach(function(field){ + field.setPeek(false) + }) + }) + } + + static setPortPeekForEdge(edge:Edge, value:boolean) : void { + const eagle = Eagle.getInstance(); + const inputPort = eagle.logicalGraph().findNodeByKeyQuiet(edge.getSrcNodeKey()).findFieldById(edge.getSrcPortId()) + const outputPort = eagle.logicalGraph().findNodeByKeyQuiet(edge.getDestNodeKey()).findFieldById(edge.getDestPortId()) + + inputPort.setPeek(value) + outputPort.setPeek(value) + } + + static edgeGetStrokeColor(edge: Edge, event: any) : string { + const eagle = Eagle.getInstance(); + + let normalColor: string = GraphConfig.getColor('edgeDefault'); + let selectedColor: string = GraphConfig.getColor('edgeDefaultSelected'); + + // check if source node is an event, if so, draw in blue + const srcNode : Node = eagle.logicalGraph().findNodeByKey(edge.getSrcNodeKey()); + + if (srcNode !== null){ + const srcPort : Field = srcNode.findFieldById(edge.getSrcPortId()); + + if (srcPort !== null && srcPort.getIsEvent()){ + normalColor = GraphConfig.getColor('edgeEvent'); + selectedColor = GraphConfig.getColor('edgeEventSelected'); + } + } + + // check if link has a warning or is invalid + const linkValid : Eagle.LinkValid = Edge.isValid(eagle, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, false, {errors:[], warnings:[]}); + + if (linkValid === Eagle.LinkValid.Invalid || linkValid === Eagle.LinkValid.Impossible){ + normalColor = GraphConfig.getColor('edgeInvalid'); + selectedColor = GraphConfig.getColor('edgeInvalidSelected'); + } + + if (linkValid === Eagle.LinkValid.Warning){ + normalColor = GraphConfig.getColor('edgeWarning'); + selectedColor = GraphConfig.getColor('edgeWarningSelected'); + } + + // check if the edge is a "closes loop" edge + if (edge.isClosesLoop()){ + normalColor = GraphConfig.getColor('edgeClosesLoop'); + selectedColor = GraphConfig.getColor('edgeClosesLoopSelected'); + } + + return eagle.objectIsSelected(edge) ? selectedColor : normalColor; + } + + static edgeGetStrokeType(edge:Edge, event:any) : string { + if(edge.isClosesLoop()){ + return ' 15, 8, 5, 8' + }else{ + return '' + } + } +} diff --git a/src/GraphUpdater.ts b/src/GraphUpdater.ts index 970663c6c..f3992bf9b 100644 --- a/src/GraphUpdater.ts +++ b/src/GraphUpdater.ts @@ -22,11 +22,12 @@ # */ +import { ActionMessage } from './Action'; import {Category} from './Category'; import {Eagle} from './Eagle'; -import {Errors} from './Errors'; import {GitHub} from './GitHub'; import {GitLab} from './GitLab'; +import { GraphChecker } from './GraphChecker'; import {LogicalGraph} from './LogicalGraph'; import {Repositories} from './Repositories'; import {Repository} from './Repository'; @@ -180,13 +181,12 @@ export class GraphUpdater { openRemoteFileFunc(row.service, row.name, row.branch, row.folder, row.file, (error: string, data: string) => { // if file fetched successfully if (error === null){ - const errorsWarnings: Errors.ErrorsWarnings = {"errors":[], "warnings":[]}; + const errors: ActionMessage[] = []; const file: RepositoryFile = new RepositoryFile(row.service, row.folder, row.file); - const lg: LogicalGraph = LogicalGraph.fromOJSJson(JSON.parse(data), file, errorsWarnings); + const lg: LogicalGraph = LogicalGraph.fromOJSJson(JSON.parse(data), file, errors); // record number of errors - row.numLoadWarnings = errorsWarnings.warnings.length; - row.numLoadErrors = errorsWarnings.errors.length; + row.numLoadErrors = errors.length; // use git-related info within file row.eagleVersion = lg.fileInfo().eagleVersion; @@ -201,9 +201,8 @@ export class GraphUpdater { row.lastModified = date.toLocaleDateString() + " " + date.toLocaleTimeString() // check the graph once loaded - const results: Errors.ErrorsWarnings = Utils.checkGraph(eagle); - row.numCheckWarnings = results.warnings.length; - row.numCheckErrors = results.errors.length; + const results: ActionMessage[] = GraphChecker.check(lg); + row.numCheckErrors = results.length; } resolve(); diff --git a/src/KeyboardShortcut.ts b/src/KeyboardShortcut.ts index 81f98626c..91c43df47 100644 --- a/src/KeyboardShortcut.ts +++ b/src/KeyboardShortcut.ts @@ -1,13 +1,13 @@ -import {Eagle} from './Eagle'; -import {Category} from './Category'; -import {Utils} from './Utils'; -import {Errors} from './Errors'; -import { Setting } from './Setting'; +import { Category } from './Category'; +import { Eagle } from './Eagle'; +import { GraphChecker } from './GraphChecker'; import { ParameterTable } from './ParameterTable'; import { QuickActions } from './QuickActions'; +import { Setting } from './Setting'; import { TutorialSystem } from './Tutorial'; +import { Utils } from './Utils'; -let currentEvent:any = null // this is used for keybord shortcut functions that need the event object to function +let currentEvent:any = null // this is used for keyboard shortcut functions that need the event object to function export class KeyboardShortcut { key: string; @@ -196,9 +196,11 @@ export class KeyboardShortcut { new KeyboardShortcut("modify_selected_edge","Modify Selected Edge", ["m"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['edit'], KeyboardShortcut.allowGraphEditing, function(){return KeyboardShortcut.edgeIsSelected && Setting.findValue(Setting.ALLOW_GRAPH_EDITING)}, (eagle): void => {eagle.editSelectedEdge();}), new KeyboardShortcut("center_graph", "Center graph", ["c"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['canvas','reset','controls'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.centerGraph();}), // NB: we need two entries for zoom_in here, the first handles '+' without shift (as found on the numpad), the second handles '+' with shift (as found sharing the '=' key) + /* new KeyboardShortcut("zoom_in", "Zoom In", ["+"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['controls','canvas'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.zoomIn();}), new KeyboardShortcut("zoom_in", "Zoom In", ["+"], "keydown", KeyboardShortcut.Modifier.Shift, KeyboardShortcut.false, ['controls','canvas'], KeyboardShortcut.false, KeyboardShortcut.true, (eagle): void => {eagle.zoomIn();}), new KeyboardShortcut("zoom_out", "Zoom Out", ["-"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['controls','canvas'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.zoomOut();}), + */ new KeyboardShortcut("toggle_left_window", "Toggle left window", ["l"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['close','open'], KeyboardShortcut.allowPaletteEditing, function(){return Setting.findValue(Setting.ALLOW_PALETTE_EDITING) || Setting.findValue(Setting.ALLOW_GRAPH_EDITING)}, (eagle): void => {eagle.leftWindow().toggleShown();}), new KeyboardShortcut("toggle_right_window", "Toggle right window", ["r"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['close','open'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.rightWindow().toggleShown();}), new KeyboardShortcut("toggle_both_window", "Toggle both windows", ["b"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['close','open'], function(){return Setting.findValue(Setting.ALLOW_PALETTE_EDITING) || Setting.findValue(Setting.ALLOW_GRAPH_EDITING)}, function(){return Setting.findValue(Setting.ALLOW_PALETTE_EDITING) || Setting.findValue(Setting.ALLOW_GRAPH_EDITING)}, (eagle): void => {eagle.toggleWindows();}), @@ -209,20 +211,22 @@ export class KeyboardShortcut { new KeyboardShortcut("open_key_parameter_table_modal", "Open Key Parameter Table Modal", ["t"], "keydown", KeyboardShortcut.Modifier.Shift, KeyboardShortcut.true, ['fields','field','node','graph','favourites'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.openParamsTableModal('keyParametersTableModal','normal');}), new KeyboardShortcut("undo", "Undo", ["z"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['back','history'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.undo().prevSnapshot(eagle)}), new KeyboardShortcut("redo", "Redo", ["z"], "keydown", KeyboardShortcut.Modifier.Shift, KeyboardShortcut.true, ['forward','history'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => {eagle.undo().nextSnapshot(eagle)}), - new KeyboardShortcut("check_graph", "Check Graph", ["!"], "keydown", KeyboardShortcut.Modifier.Shift, KeyboardShortcut.true, ['error','errors','fix'], KeyboardShortcut.allowGraphEditing, function(){return KeyboardShortcut.graphNotEmpty && Setting.findValue(Setting.ALLOW_GRAPH_EDITING) }, (eagle): void => {eagle.showGraphErrors();}), + new KeyboardShortcut("check_graph", "Check Graph", ["!"], "keydown", KeyboardShortcut.Modifier.Shift, KeyboardShortcut.true, ['error','errors','fix'], KeyboardShortcut.allowGraphEditing, function(){return KeyboardShortcut.graphNotEmpty && Setting.findValue(Setting.ALLOW_GRAPH_EDITING) }, (eagle): void => {eagle.openCheckGraphModal();}), new KeyboardShortcut("open_repository", "Open Repository", ["1"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['tab','tabs','window','menu','right'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => { eagle.rightWindow().shown(true).mode(Eagle.RightWindowMode.Repository)}), new KeyboardShortcut("open_translation", "Open Translation", ["4"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['tab','tabs','window','menu','right'], function(){return Setting.findValue(Setting.USER_TRANSLATOR_MODE) != Setting.TranslatorMode.Minimal}, function(){return Setting.findValue(Setting.USER_TRANSLATOR_MODE) != Setting.TranslatorMode.Minimal}, (eagle): void => { eagle.rightWindow().shown(true).mode(Eagle.RightWindowMode.TranslationMenu)}), new KeyboardShortcut("open_inspector", "Open Inspector", ["3"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['tab','tabs','window','menu','right'], KeyboardShortcut.true, KeyboardShortcut.somethingIsSelected, (eagle): void => { eagle.rightWindow().shown(true).mode(Eagle.RightWindowMode.Inspector)}), new KeyboardShortcut("open_hierarchy", "Open Hierarchy", ["2"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['tab','tabs','window','menu','right'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => { eagle.rightWindow().shown(true).mode(Eagle.RightWindowMode.Hierarchy)}), + /* new KeyboardShortcut("toggle_show_data_nodes", "Toggle Show Data Nodes", ["j"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['hide','node','canvas'], KeyboardShortcut.true, KeyboardShortcut.true, (eagle): void => { eagle.toggleShowDataNodes(); }), new KeyboardShortcut("toggle_snap_to_grid", "Toggle Snap to Grid", ["y"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['canvas'], KeyboardShortcut.allowGraphEditing, KeyboardShortcut.allowGraphEditing, (eagle): void => { eagle.toggleSnapToGrid(); }), + */ new KeyboardShortcut("check_for_component_updates", "Check for Component Updates", ["q"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['nodes'], KeyboardShortcut.allowGraphEditing, function(){return KeyboardShortcut.graphNotEmpty && Setting.findValue(Setting.ALLOW_GRAPH_EDITING)}, (eagle): void => { eagle.checkForComponentUpdates(); }), new KeyboardShortcut("copy_from_graph_without_children", "Copy from graph without children", ["c"], "keydown", KeyboardShortcut.Modifier.Shift, KeyboardShortcut.true, [''], KeyboardShortcut.allowGraphEditing, KeyboardShortcut.allowGraphEditing, (eagle): void => { eagle.copySelectionToClipboard(false); }), new KeyboardShortcut("copy_from_graph", "Copy from graph", ["c"], "keydown", KeyboardShortcut.Modifier.Ctrl, KeyboardShortcut.true, [''], KeyboardShortcut.allowGraphEditing, KeyboardShortcut.allowGraphEditing, (eagle): void => { eagle.copySelectionToClipboard(true); }), new KeyboardShortcut("paste_to_graph", "Paste to graph", ["v"], "keydown", KeyboardShortcut.Modifier.Ctrl, KeyboardShortcut.true, [''], KeyboardShortcut.allowGraphEditing, KeyboardShortcut.allowGraphEditing, (eagle): void => { eagle.pasteFromClipboard(); }), new KeyboardShortcut("select_all_in_graph", "Select all in graph", ["a"], "keydown", KeyboardShortcut.Modifier.Ctrl, KeyboardShortcut.true, [''], KeyboardShortcut.true, KeyboardShortcut.graphNotEmpty, (eagle): void => { eagle.selectAllInGraph(); }), new KeyboardShortcut("select_none_in_graph", "Select none in graph", ["Escape"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, ['deselect'], KeyboardShortcut.true, KeyboardShortcut.somethingIsSelected, (eagle): void => { eagle.selectNoneInGraph(); }), - new KeyboardShortcut("fix_all", "Fix all errors in graph", ["f"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, [''], KeyboardShortcut.allowGraphEditing, KeyboardShortcut.allowGraphEditing, (eagle): void => { Errors.fixAll(); }), + new KeyboardShortcut("fix_all", "Fix all errors in graph", ["f"], "keydown", KeyboardShortcut.Modifier.None, KeyboardShortcut.true, [''], KeyboardShortcut.allowGraphEditing, KeyboardShortcut.allowGraphEditing, (eagle): void => { GraphChecker.fixAll(); }), new KeyboardShortcut("table_move_down", "Table move down one cell", ["Enter"], "keydown", KeyboardShortcut.Modifier.Input, KeyboardShortcut.true, ['controls'], KeyboardShortcut.false, KeyboardShortcut.showTableModal, (eagle): void => { ParameterTable.tableEnterShortcut(currentEvent);}), ]; } diff --git a/src/LogicalGraph.ts b/src/LogicalGraph.ts index e7c70f916..62ddd1a7e 100644 --- a/src/LogicalGraph.ts +++ b/src/LogicalGraph.ts @@ -24,13 +24,14 @@ import * as ko from "knockout"; +import { ActionMessage } from "./Action"; import { Category } from './Category'; import { Daliuge } from "./Daliuge"; import { Eagle } from './Eagle'; import { Edge } from './Edge'; -import { Errors } from './Errors'; import { Field } from './Field'; import { FileInfo } from './FileInfo'; +import { GraphConfig } from "./graphConfig"; import { GraphUpdater } from './GraphUpdater'; import { Node } from './Node'; import { RepositoryFile } from './RepositoryFile'; @@ -39,14 +40,14 @@ import { Utils } from './Utils'; export class LogicalGraph { fileInfo : ko.Observable; - private nodes : Node[]; - private edges : Edge[]; + private nodes : ko.ObservableArray; + private edges : ko.ObservableArray; constructor(){ this.fileInfo = ko.observable(new FileInfo()); this.fileInfo().type = Eagle.FileType.Graph; - this.nodes = []; - this.edges = []; + this.nodes = ko.observableArray([]); + this.edges = ko.observableArray([]); } static toOJSJson = (graph : LogicalGraph, forTranslation : boolean) : object => { @@ -82,6 +83,16 @@ export class LogicalGraph { const srcNode = graph.findNodeByKey(srcKey); const destNode = graph.findNodeByKey(destKey); + // if source and destination node could not be found, skip edge + if (srcNode === null){ + console.warn("Could not find edge (", srcKey, "->", destKey, ") source node by key (", srcKey, "), skipping"); + continue; + } + if (destNode === null){ + console.warn("Could not find edge (", srcKey, "->", destKey, ") destination node by key (", destKey, "), skipping"); + continue; + } + // for OJS format, we actually store links using the node keys of the construct, not the node keys of the embedded applications if (srcNode.isEmbedded()){ srcKey = srcNode.getEmbedKey(); @@ -114,24 +125,27 @@ export class LogicalGraph { return result; } - static fromOJSJson = (dataObject : any, file : RepositoryFile, errorsWarnings : Errors.ErrorsWarnings) : LogicalGraph => { + static fromOJSJson = (dataObject: any, file: RepositoryFile, errors: ActionMessage[]) : LogicalGraph => { + console.log("LG.fromOJSJson()", file.name); + // create new logical graph object const result : LogicalGraph = new LogicalGraph(); // copy modelData into fileInfo - result.fileInfo(FileInfo.fromOJSJson(dataObject.modelData, errorsWarnings)); + result.fileInfo(FileInfo.fromOJSJson(dataObject.modelData, errors)); // add nodes for (const nodeData of dataObject.nodeDataArray){ const extraUsedKeys: number[] = []; - const newNode = Node.fromOJSJson(nodeData, errorsWarnings, false, (): number => { - const resultKeys: number[] = Utils.getUsedKeys(result.nodes); + const newNode = Node.fromOJSJson(nodeData, errors, false, (): number => { + const resultKeys: number[] = Utils.getUsedKeys(result.nodes()); const nodeDataKeys: number[] = Utils.getUsedKeysFromNodeData(dataObject.nodeDataArray); const combinedKeys: number[] = resultKeys.concat(nodeDataKeys.concat(extraUsedKeys)); const newKey = Utils.findNewKey(combinedKeys); + extraUsedKeys.push(newKey); return newKey; }); @@ -152,13 +166,13 @@ export class LogicalGraph { const parentIndex = GraphUpdater.findIndexOfNodeDataArrayWithKey(dataObject.nodeDataArray, nodeData.group); if (parentIndex !== -1){ - result.nodes[i].setParentKey(result.nodes[parentIndex].getKey()); + result.nodes()[i].setParentKey(result.nodes()[parentIndex].getKey()); } } // add edges for (const linkData of dataObject.linkDataArray){ - const newEdge = Edge.fromOJSJson(linkData, errorsWarnings); + const newEdge = Edge.fromOJSJson(linkData, errors); if (newEdge === null){ continue; @@ -169,15 +183,15 @@ export class LogicalGraph { // check for missing name if (result.fileInfo().name === ""){ - const error : string = "FileInfo.name is empty. Setting name to " + file.name; - errorsWarnings.warnings.push(Errors.Message(error)); + const warning : string = "FileInfo.name is empty. Setting name to " + file.name; + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, warning)); result.fileInfo().name = file.name; } // add a step here to check that no edges are incident on constructs, and move any edges found to the embedded applications // add warnings to errorsWarnings - for (const edge of result.edges){ + for (const edge of result.edges()){ // get references to actual source and destination nodes (from the keys) const sourceNode : Node = result.findNodeByKey(edge.getSrcNodeKey()); const destinationNode : Node = result.findNodeByKey(edge.getDestNodeKey()); @@ -186,13 +200,13 @@ export class LogicalGraph { if (sourceNode.getCategoryType() === Category.Type.Construct){ const srcKeyAndPort = sourceNode.findPortInApplicationsById(edge.getSrcPortId()); const warning = "Updated source node of edge " + edge.getId() + " from construct " + edge.getSrcNodeKey() + " to embedded application " + srcKeyAndPort.key; - errorsWarnings.warnings.push(Errors.Message(warning)); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, warning)); edge.setSrcNodeKey(srcKeyAndPort.key); } if (destinationNode.getCategoryType() === Category.Type.Construct){ const destKeyAndPort = destinationNode.findPortInApplicationsById(edge.getDestPortId()); const warning = "Updated destination node of edge " + edge.getId() + " from construct " + edge.getDestNodeKey() + " to embedded application " + destKeyAndPort.key; - errorsWarnings.warnings.push(Errors.Message(warning)); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, warning)); edge.setDestNodeKey(destKeyAndPort.key); } } @@ -203,182 +217,6 @@ export class LogicalGraph { return result; } -/* - static toV3Json = (graph : LogicalGraph) : object => { - const result : any = {}; - - result.DALiuGEGraph = {}; - const dlgg = result.DALiuGEGraph; - - // top level element info - dlgg.type = Eagle.DALiuGEFileType.LogicalGraph; - dlgg.name = graph.fileInfo().name; - dlgg.schemaVersion = Eagle.DALiuGESchemaVersion.V3; - dlgg.commitHash = graph.fileInfo().sha; - dlgg.repositoryService = graph.fileInfo().repositoryService; - dlgg.repositoryBranch = graph.fileInfo().repositoryBranch; - dlgg.repositoryName = graph.fileInfo().repositoryName; - dlgg.repositoryPath = graph.fileInfo().path; - - // add nodes - dlgg.nodeData = {}; - for (let i = 0 ; i < graph.getNodes().length ; i++){ - const node : Node = graph.getNodes()[i]; - const nodeData : any = Node.toV3NodeJson(node, i); - - dlgg.nodeData[node.getKey()] = nodeData; - } - - // add links - dlgg.linkData = {}; - for (let i = 0 ; i < graph.getEdges().length ; i++){ - const edge : Edge = graph.getEdges()[i]; - const linkData : any = Edge.toV3Json(edge); - - dlgg.linkData[i] = linkData; - } - - // add components - dlgg.componentData = {}; - for (const node of graph.getNodes()){ - dlgg.componentData[node.getKey()] = Node.toV3ComponentJson(node); - } - - return result; - } - */ - -/* - static fromV3Json = (dataObject : any, file : RepositoryFile, errorsWarnings : Eagle.ErrorsWarnings) : LogicalGraph => { - const result: LogicalGraph = new LogicalGraph(); - const dlgg = dataObject.DALiuGEGraph; - - result.fileInfo().type = dlgg.type; - result.fileInfo().name = dlgg.name; - result.fileInfo().schemaVersion = dlgg.schemaVersion; - result.fileInfo().sha = dlgg.commitHash; - result.fileInfo().repositoryService = dlgg.repositoryService; - result.fileInfo().repositoryBranch = dlgg.repositoryBranch; - result.fileInfo().repositoryName = dlgg.repositoryName; - result.fileInfo().path = dlgg.repositoryPath; - - for (const key in dlgg.nodeData){ - const node = Node.fromV3NodeJson(dlgg.nodeData[key], key, errorsWarnings); - - Node.fromV3ComponentJson(dlgg.componentData[key], node, errorsWarnings); - - result.nodes.push(node); - } - - for (const key in dlgg.linkData){ - const edge = Edge.fromV3Json(dlgg.linkData[key], errorsWarnings); - result.edges.push(edge); - } - - return result; - } -*/ -/* - static toAppRefJson = (graph : LogicalGraph) : object => { - const result : any = {}; - - result.modelData = FileInfo.toOJSJson(graph.fileInfo()); - result.modelData.schemaVersion = Eagle.DALiuGESchemaVersion.AppRef; - result.modelData.numLGNodes = graph.getNodes().length; - - // add nodes - result.nodeDataArray = []; - for (const node of graph.getNodes()){ - const nodeData : any = Node.toAppRefJson(node); - result.nodeDataArray.push(nodeData); - } - - // add embedded nodes - for (let i = 0 ; i < graph.getNodes().length ; i++){ - const node : Node = graph.getNodes()[i]; - - if (node.hasInputApplication()){ - const nodeData : any = Node.toAppRefJson(node.getInputApplication()); - - // update ref in parent - result.nodeDataArray[i].inputApplicationRef = nodeData.key; - - // add child to nodeDataArray - result.nodeDataArray.push(nodeData); - } - - if (node.hasOutputApplication()){ - const nodeData : any = Node.toAppRefJson(node.getOutputApplication()); - - // update ref in parent - result.nodeDataArray[i].outputApplicationRef = nodeData.key; - - // add child to nodeDataArray - result.nodeDataArray.push(nodeData); - } - } - - // add links - result.linkDataArray = []; - for (const edge of graph.getEdges()){ - result.linkDataArray.push(Edge.toAppRefJson(edge, graph)); - } - - return result; - } -*/ -/* - static fromAppRefJson = (dataObject : any, file : RepositoryFile, errorsWarnings : Eagle.ErrorsWarnings) : LogicalGraph => { - // create new logical graph object - const result : LogicalGraph = new LogicalGraph(); - - // copy modelData into fileInfo - result.fileInfo(FileInfo.fromOJSJson(dataObject.modelData, errorsWarnings)); - - // add nodes - for (const nodeData of dataObject.nodeDataArray){ - let node; - - // check if node is an embedded node, if so, don't push to nodes array - if (nodeData.embedKey === null){ - node = Node.fromAppRefJson(nodeData, errorsWarnings); - } else { - // skip node - continue; - } - - // check if this node has an embedded input application, if so, find and copy it now - if (typeof nodeData.inputApplicationRef !== 'undefined'){ - const inputAppNodeData = LogicalGraph._findNodeDataWithKey(dataObject.nodeDataArray, nodeData.inputApplicationRef); - node.setInputApplication(Node.fromAppRefJson(inputAppNodeData, errorsWarnings)); - } - // check if this node has an embedded output application, if so, find and copy it now - if (typeof nodeData.outputApplicationRef !== 'undefined'){ - const outputAppNodeData = LogicalGraph._findNodeDataWithKey(dataObject.nodeDataArray, nodeData.outputApplicationRef); - node.setOutputApplication(Node.fromAppRefJson(outputAppNodeData, errorsWarnings)); - } - - result.nodes.push(node); - } - - // add edges - for (const linkData of dataObject.linkDataArray){ - result.edges.push(Edge.fromAppRefJson(linkData, errorsWarnings)); - } - - // check for missing name - if (result.fileInfo().name === ""){ - const error : string = "FileInfo.name is empty. Setting name to " + file.name; - console.warn(error); - errorsWarnings.errors.push(error); - - result.fileInfo().name = file.name; - } - - return result; - } -*/ - static _findNodeDataWithKey = (nodeDataArray: any[], key: number): any => { for (const nodeData of nodeDataArray){ if (nodeData.key === key){ @@ -393,11 +231,27 @@ export class LogicalGraph { } getNodes = () : Node[] => { - return this.nodes; + return this.nodes(); + } + + getAllNodes = () : Node[] => { + const nodes : Node[] =[] + this.nodes().forEach(function(node){ + nodes.push(node) + if(node.isConstruct()){ + if(node.getInputApplication()!= null){ + nodes.push(node.getInputApplication()) + } + if(node.getOutputApplication() != null){ + nodes.push(node.getOutputApplication()) + } + } + }) + return nodes; } getNumNodes = () : number => { - return this.nodes.length; + return this.nodes().length; } addEdgeComplete = (edge : Edge) => { @@ -405,17 +259,29 @@ export class LogicalGraph { } getEdges = () : Edge[] => { - return this.edges; + return this.edges(); } getNumEdges = () : number => { - return this.edges.length; + return this.edges().length; + } + + getCommentNodes = () : Node[] => { + const commentNodes: Node[] = []; + + for (const node of this.getNodes()){ + if (node.isComment()){ + commentNodes.push(node); + } + } + + return commentNodes; } countEdgesIncidentOnNode = (node : Node) : number => { let result: number = 0; - for (const edge of this.edges){ + for (const edge of this.edges()){ if ((edge.getSrcNodeKey() === node.getKey() ) || ( edge.getDestNodeKey() === node.getKey() )){ result += 1; } @@ -427,8 +293,8 @@ export class LogicalGraph { clear = () : void => { this.fileInfo().clear(); this.fileInfo().type = Eagle.FileType.Graph; - this.nodes = []; - this.edges = []; + this.nodes([]); + this.edges([]); } clone = () : LogicalGraph => { @@ -437,12 +303,12 @@ export class LogicalGraph { result.fileInfo(this.fileInfo().clone()); // copy nodes - for (const node of this.nodes){ + for (const node of this.nodes()){ result.nodes.push(node.clone()); } // copy edges - for (const edge of this.edges){ + for (const edge of this.edges()){ result.edges.push(edge.clone()); } @@ -502,24 +368,22 @@ export class LogicalGraph { } findNodeByKey = (key : number) : Node => { - for (let i = this.nodes.length - 1; i >= 0 ; i--){ - + for (let i = this.nodes().length - 1; i >= 0 ; i--){ // check if the node itself has a matching key - if (this.nodes[i].getKey() === key){ - return this.nodes[i]; + if (this.nodes()[i].getKey() === key){ + return this.nodes()[i]; } - // check if the node's inputApp has a matching key - if (this.nodes[i].hasInputApplication()){ - if (this.nodes[i].getInputApplication().getKey() === key){ - return this.nodes[i].getInputApplication(); + if (this.nodes()[i].hasInputApplication()){ + if (this.nodes()[i].getInputApplication().getKey() === key){ + return this.nodes()[i].getInputApplication(); } } // check if the node's outputApp has a matching key - if (this.nodes[i].hasOutputApplication()){ - if (this.nodes[i].getOutputApplication().getKey() === key){ - return this.nodes[i].getOutputApplication(); + if (this.nodes()[i].hasOutputApplication()){ + if (this.nodes()[i].getOutputApplication().getKey() === key){ + return this.nodes()[i].getOutputApplication(); } } } @@ -529,24 +393,24 @@ export class LogicalGraph { findNodeByKeyQuiet = (key : number) : Node => { //used temporarily for the table modals to prevent console spam relating to too many calls when changing selected objects - for (let i = this.nodes.length - 1; i >= 0 ; i--){ + for (let i = this.nodes().length - 1; i >= 0 ; i--){ // check if the node itself has a matching key - if (this.nodes[i].getKey() === key){ - return this.nodes[i]; + if (this.nodes()[i].getKey() === key){ + return this.nodes()[i]; } // check if the node's inputApp has a matching key - if (this.nodes[i].hasInputApplication()){ - if (this.nodes[i].getInputApplication().getKey() === key){ - return this.nodes[i].getInputApplication(); + if (this.nodes()[i].hasInputApplication()){ + if (this.nodes()[i].getInputApplication().getKey() === key){ + return this.nodes()[i].getInputApplication(); } } // check if the node's outputApp has a matching key - if (this.nodes[i].hasOutputApplication()){ - if (this.nodes[i].getOutputApplication().getKey() === key){ - return this.nodes[i].getOutputApplication(); + if (this.nodes()[i].hasOutputApplication()){ + if (this.nodes()[i].getOutputApplication().getKey() === key){ + return this.nodes()[i].getOutputApplication(); } } } @@ -554,24 +418,24 @@ export class LogicalGraph { } findNodeById = (id : string) : Node => { - for (let i = this.nodes.length - 1; i >= 0 ; i--){ + for (let i = this.nodes().length - 1; i >= 0 ; i--){ // check if the node itself has a matching key - if (this.nodes[i].getId() === id){ - return this.nodes[i]; + if (this.nodes()[i].getId() === id){ + return this.nodes()[i]; } // check if the node's inputApp has a matching key - if (this.nodes[i].hasInputApplication()){ - if (this.nodes[i].getInputApplication().getId() === id){ - return this.nodes[i].getInputApplication(); + if (this.nodes()[i].hasInputApplication()){ + if (this.nodes()[i].getInputApplication().getId() === id){ + return this.nodes()[i].getInputApplication(); } } // check if the node's outputApp has a matching key - if (this.nodes[i].hasOutputApplication()){ - if (this.nodes[i].getOutputApplication().getId() === id){ - return this.nodes[i].getOutputApplication(); + if (this.nodes()[i].hasOutputApplication()){ + if (this.nodes()[i].getOutputApplication().getId() === id){ + return this.nodes()[i].getOutputApplication(); } } } @@ -584,7 +448,7 @@ export class LogicalGraph { let graphNodeId:string eagle.logicalGraph().getNodes().forEach(function(node){ if(node.getName() === name){ - graphNodeId = node.getGraphNodeId() + graphNodeId = node.getId() } }) return graphNodeId @@ -623,42 +487,42 @@ export class LogicalGraph { } // search through nodes in graph, looking for one with the correct key - for (let i = this.nodes.length - 1; i >= 0 ; i--){ + for (let i = this.nodes().length - 1; i >= 0 ; i--){ // delete the node - if (this.nodes[i].getKey() === key){ + if (this.nodes()[i].getKey() === key){ this.nodes.splice(i, 1); continue; } // delete the input application - if (this.nodes[i].hasInputApplication() && this.nodes[i].getInputApplication().getKey() === key){ - this.nodes[i].setInputApplication(null); + if (this.nodes()[i].hasInputApplication() && this.nodes()[i].getInputApplication().getKey() === key){ + this.nodes()[i].setInputApplication(null); } // delete the output application - if (this.nodes[i].hasOutputApplication() && this.nodes[i].getOutputApplication().getKey() === key){ - this.nodes[i].setOutputApplication(null); + if (this.nodes()[i].hasOutputApplication() && this.nodes()[i].getOutputApplication().getKey() === key){ + this.nodes()[i].setOutputApplication(null); } } // delete children - for (let i = this.nodes.length - 1; i >= 0 ; i--){ + for (let i = this.nodes().length - 1; i >= 0 ; i--){ // check that iterator still points to a valid element in the nodes array // a check like this wouldn't normally be necessary, but we are deleting elements from the array within the loop, so it might be shorter than we expect - if (i >= this.nodes.length){ + if (i >= this.nodes().length){ continue; } - if (this.nodes[i].getParentKey() === key){ - this.removeNode(this.nodes[i]); + if (this.nodes()[i].getParentKey() === key){ + this.removeNode(this.nodes()[i]); } } } findEdgeById = (id: string) : Edge => { - for (let i = this.edges.length - 1; i >= 0 ; i--){ - if (this.edges[i].getId() === id){ - return this.edges[i]; + for (let i = this.edges().length - 1; i >= 0 ; i--){ + if (this.edges()[i].getId() === id){ + return this.edges()[i]; } } return null; @@ -667,8 +531,8 @@ export class LogicalGraph { removeEdgeById = (id: string) : void => { let found = false; - for (let i = this.edges.length - 1; i >= 0 ; i--){ - if (this.edges[i].getId() === id){ + for (let i = this.edges().length - 1; i >= 0 ; i--){ + if (this.edges()[i].getId() === id){ found = true; this.edges.splice(i, 1); } @@ -681,23 +545,29 @@ export class LogicalGraph { // delete edges that start from or end at the node with the given key removeEdgesByKey = (key: number) : void => { - for (let i = this.edges.length - 1 ; i >= 0; i--){ - const edge : Edge = this.edges[i]; + for (let i = this.edges().length - 1 ; i >= 0; i--){ + const edge : Edge = this.edges()[i]; if (edge.getSrcNodeKey() === key || edge.getDestNodeKey() === key){ this.edges.splice(i, 1); } } } - portIsLinked = (nodeKey : number, portId : string) : boolean => { - for (const edge of this.edges){ - if (edge.getSrcNodeKey() === nodeKey && edge.getSrcPortId() === portId || - edge.getDestNodeKey() === nodeKey && edge.getDestPortId() === portId){ - return true; + portIsLinked = (nodeKey : number, portId : string) : any => { + let result:{input:boolean,output:boolean} = {'input':false,'output':false} + let input = false + let output = false + for (const edge of this.edges()){ + if(edge.getSrcNodeKey() === nodeKey && edge.getSrcPortId() === portId){ + output = true + } + if(edge.getDestNodeKey() === nodeKey && edge.getDestPortId() === portId){ + input = true } } + result= {'input':input,'output':output} - return false; + return result ; } // TODO: shrinkNode and normaliseNodes seem to share some common code, maybe factor out or combine? @@ -725,32 +595,31 @@ export class LogicalGraph { if (n.getPosition().y < minY){ minY = n.getPosition().y; } - if (n.getPosition().x + n.getWidth() > maxX){ - maxX = n.getPosition().x + n.getWidth(); + if (n.getPosition().x + n.getRadius() > maxX){ + maxX = n.getPosition().x + n.getRadius(); } - if (n.getPosition().y + n.getHeight() > maxY){ - maxY = n.getPosition().y + n.getHeight(); + if (n.getPosition().y + n.getRadius() > maxY){ + maxY = n.getPosition().y + n.getRadius(); } } } // if no children were found, set to default size if (numChildren === 0){ - node.setWidth(Node.DEFAULT_WIDTH); - node.setHeight(Node.DEFAULT_HEIGHT); + node.setRadius(GraphConfig.MINIMUM_CONSTRUCT_RADIUS); return; } // add some padding - minX -= Node.CONSTRUCT_MARGIN_LEFT; - minY -= Node.CONSTRUCT_MARGIN_TOP; - maxX += Node.CONSTRUCT_MARGIN_RIGHT; - maxY += Node.CONSTRUCT_MARGIN_BOTTOM; + minX -= GraphConfig.CONSTRUCT_MARGIN; + minY -= GraphConfig.CONSTRUCT_MARGIN; + maxX += GraphConfig.CONSTRUCT_MARGIN; + maxY += GraphConfig.CONSTRUCT_MARGIN; // set the size of the node node.setPosition(minX, minY); - node.setWidth(maxX - minX); - node.setHeight(maxY - minY); + const maxDimension = Math.max(maxX - minX, maxY - minY); + node.setRadius(maxDimension); } findMultiplicity = (node : Node) : number => { @@ -782,11 +651,11 @@ export class LogicalGraph { return result; } - checkForNodeAt = (x: number, y: number, width: number, height: number, ignoreKey: number, groupsOnly: boolean = false) : Node => { + checkForNodeAt = (x: number, y: number, radius: number, ignoreKey: number, groupsOnly: boolean = false) : Node => { const overlaps : Node[] = []; // find all the overlapping nodes - for (const node of this.nodes){ + for (const node of this.nodes()){ // abort if checking for self! if (node.getKey() === ignoreKey){ continue; @@ -797,7 +666,7 @@ export class LogicalGraph { continue; } - if (Utils.nodesOverlap(x, y, width, height, node.getPosition().x, node.getPosition().y, node.getWidth(), node.getHeight())){ + if (Utils.nodesOverlap(x, y, radius, node.getPosition().x, node.getPosition().y, node.getRadius())){ overlaps.push(node); } } @@ -846,7 +715,7 @@ export class LogicalGraph { getChildrenOfNodeByKey = (key: number) : Node[] => { const result: Node[] = []; - for (const node of this.nodes){ + for (const node of this.nodes()){ if (node.getParentKey() === key){ result.push(node); } @@ -855,6 +724,32 @@ export class LogicalGraph { return result; } + getNodesDrawOrdered : ko.PureComputed = ko.pureComputed(() => { + const indexPlusDepths : {index:number, depth:number}[] = []; + const result : Node[] = []; + + // populate index plus depths + for (let i = 0 ; i < this.nodes().length ; i++){ + const node = this.getNodes()[i]; + + const depth = this.findDepthByKey(node.getKey()); + + indexPlusDepths.push({index:i, depth:depth}); + } + + // sort nodes in depth ascending + indexPlusDepths.sort(function(a, b){ + return a.depth - b.depth; + }); + + // write nodes to result in sorted order + for (const indexPlusDepth of indexPlusDepths){ + result.push(this.getNodes()[indexPlusDepth.index]); + } + + return result; + }, this); + static normaliseNodes = (nodes: Node[]) : {x: number, y: number} => { let minX = Number.MAX_SAFE_INTEGER; let maxX = Number.MIN_SAFE_INTEGER; @@ -871,21 +766,21 @@ export class LogicalGraph { minY = node.getPosition().y; } - if (node.getPosition().x + node.getWidth() > maxX){ - maxX = node.getPosition().x + node.getWidth(); + if (node.getPosition().x + node.getRadius() > maxX){ + maxX = node.getPosition().x + node.getRadius(); } - if (node.getPosition().y + node.getHeight() > maxY){ - maxY = node.getPosition().y + node.getHeight(); + if (node.getPosition().y + node.getRadius() > maxY){ + maxY = node.getPosition().y + node.getRadius(); } } // move all nodes so that the top left corner of the graph starts at the origin 0,0 for (const node of nodes){ const pos = node.getPosition(); - node.setPosition(pos.x - minX + Node.CONSTRUCT_MARGIN_LEFT, pos.y - minY + Node.CONSTRUCT_MARGIN_TOP); + node.setPosition(pos.x - minX + GraphConfig.CONSTRUCT_MARGIN, pos.y - minY + GraphConfig.CONSTRUCT_MARGIN); } - return {x: maxX - minX + Node.CONSTRUCT_MARGIN_LEFT + Node.CONSTRUCT_MARGIN_RIGHT, y: maxY - minY + Node.CONSTRUCT_MARGIN_TOP + Node.CONSTRUCT_MARGIN_BOTTOM}; + return {x: maxX - minX + GraphConfig.CONSTRUCT_MARGIN + GraphConfig.CONSTRUCT_MARGIN, y: maxY - minY + GraphConfig.CONSTRUCT_MARGIN + GraphConfig.CONSTRUCT_MARGIN}; } } diff --git a/src/Modals.ts b/src/Modals.ts index 17c28a190..de042dfeb 100644 --- a/src/Modals.ts +++ b/src/Modals.ts @@ -335,14 +335,14 @@ export class Modals { // otherwise, check the current project, and load all selected palettes for (const ep of eagle.explorePalettes().getProject().palettes()){ if (ep.isSelected()){ - eagle.openRemoteFile(new RepositoryFile(new Repository(ep.repositoryService, ep.repositoryName, ep.repositoryBranch, false), ep.path, ep.name)); + eagle.openRemoteFile(new RepositoryFile(new Repository(ep.repositoryService, ep.repositoryName, ep.repositoryBranch, false), ep.path, ep.name), null); } } }); $('#parameterTableModal').on('hidden.bs.modal', function(){ eagle.showTableModal(false) - eagle.checkGraph(); + eagle.graphChecker().check(); }); $('#parameterTableModal').on('shown.bs.modal', function(){ diff --git a/src/Node.ts b/src/Node.ts index b11675ec4..84649eae7 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -24,43 +24,31 @@ import * as ko from "knockout"; -import {Utils} from './Utils'; -import {GraphUpdater} from './GraphUpdater'; -import {Eagle} from './Eagle'; -import {Field} from './Field'; -import {Errors} from './Errors'; -import {Category} from './Category'; -import {CategoryData} from './CategoryData'; -import {Setting} from './Setting'; -import {Daliuge} from './Daliuge'; +import { ActionList } from "./ActionList"; +import { ActionMessage } from "./Action"; +import { Category } from './Category'; +import { CategoryData } from './CategoryData'; +import { Daliuge } from './Daliuge'; +import { Eagle } from './Eagle'; +import { Field } from './Field'; +import { GraphRenderer } from "./GraphRenderer"; +import { Setting } from './Setting'; +import { Utils } from './Utils'; +import { GraphConfig } from "./graphConfig"; export class Node { - private _id : string + private _id : ko.Observable; private key : ko.Observable; private name : ko.Observable; private description : ko.Observable; - private x : number; // display position - private y : number; - private realX : number; // underlying position (pre snap-to-grid) - private realY : number; - - - private width : number; - private height : number; - private color : ko.Observable; - private drawOrderHint : ko.Observable; // a secondary sorting hint when ordering the nodes for drawing - // (primary method is using parent-child relationships) - // a node with greater drawOrderHint is always in front of an element with a lower drawOrderHint + private x : ko.Observable; + private y : ko.Observable; + // private realX : number; // underlying position (pre snap-to-grid) + // private realY : number; private parentKey : ko.Observable; private embedKey : ko.Observable; - private collapsed : ko.Observable; // indicates whether the node is shown collapsed in the graph display - private expanded : ko.Observable; // true, if the node has been expanded in the hierarchy tab in EAGLE - private keepExpanded : ko.Observable; //states if a node in the hierarchy is forced Open. groups that contain nodes that a drawn edge is connecting to are kept open - - private peek : boolean; // true if we are temporarily showing the ports based on the users mouse position - private flipPorts : ko.Observable; private inputApplication : ko.Observable; private outputApplication : ko.Observable; @@ -77,78 +65,76 @@ export class Node { private paletteDownloadUrl : ko.Observable; private dataHash : ko.Observable; - public static readonly DEFAULT_WIDTH : number = 200; - public static readonly DEFAULT_HEIGHT : number = 72; - public static readonly MINIMUM_WIDTH : number = 200; - public static readonly MINIMUM_HEIGHT : number = 72; - public static readonly DEFAULT_COLOR : string = "ffffff"; - public static readonly GROUP_DEFAULT_WIDTH : number = 400; - public static readonly GROUP_DEFAULT_HEIGHT : number = 200; - public static readonly GROUP_COLLAPSED_WIDTH : number = 128; - public static readonly GROUP_COLLAPSED_HEIGHT : number = 128; - public static readonly DATA_COMPONENT_WIDTH : number = 48; - public static readonly DATA_COMPONENT_HEIGHT : number = 48; + public static readonly DEFAULT_COLOR : string = "ffffff"; - public static readonly NO_APP_STRING : string = ""; + public static readonly NO_APP_STRING : string = "-no app-"; + public static readonly NO_APP_NAME_STRING : string = "-no name-"; - // when creating a new construct to enclose a selection, or shrinking a node to enclose its children, - // this is the default margin that should be left on each side - public static readonly CONSTRUCT_MARGIN_LEFT: number = 24; - public static readonly CONSTRUCT_MARGIN_RIGHT: number = 24; - public static readonly CONSTRUCT_MARGIN_TOP: number = 72; - public static readonly CONSTRUCT_MARGIN_BOTTOM: number = 16; + //graph related things + private collapsed : ko.Observable; // indicates whether the node is shown collapsed in the graph display + private expanded : ko.Observable; // true, if the node has been expanded in the hierarchy tab in EAGLE + private keepExpanded : ko.Observable; //states if a node in the hierarchy is forced Open. groups that contain nodes that a drawn edge is connecting to are kept open + private peek : ko.Observable; // true if we are temporarily showing the ports based on the users mouse position + private radius : ko.Observable; + + private color : ko.Observable; + private drawOrderHint : ko.Observable; // a secondary sorting hint when ordering the nodes for drawing + // (primary method is using parent-child relationships) + // a node with greater drawOrderHint is always in front of an element with a lower drawOrderHint constructor(key : number, name : string, description : string, category : Category){ - this._id = Utils.uuidv4(); + this._id = ko.observable(Utils.uuidv4()); this.key = ko.observable(key); this.name = ko.observable(name); this.description = ko.observable(description); - - // display position - this.x = 0; - this.y = 0; - this.realX = 0; - this.realY = 0; - this.width = Node.DEFAULT_WIDTH; - this.height = Node.DEFAULT_HEIGHT; - this.color = ko.observable(Utils.getColorForNode(category)); - this.drawOrderHint = ko.observable(0); + this.x = ko.observable(0); + this.y = ko.observable(0); + // display position + // this.realX = 0; + // this.realY = 0; + + this.key = ko.observable(key); + this.name = ko.observable(name); + this.description = ko.observable(description); this.parentKey = ko.observable(null); this.embedKey = ko.observable(null); - this.collapsed = ko.observable(true); - this.peek = false; - this.flipPorts = ko.observable(false); this.inputApplication = ko.observable(null); this.outputApplication = ko.observable(null); this.fields = ko.observableArray([]); - this.category = ko.observable(category); // lookup correct categoryType based on category this.categoryType = ko.observable(CategoryData.getCategoryData(category).categoryType); - this.subject = ko.observable(null); - this.expanded = ko.observable(true); - this.keepExpanded = ko.observable(false); - this.repositoryUrl = ko.observable(""); this.commitHash = ko.observable(""); this.paletteDownloadUrl = ko.observable(""); this.dataHash = ko.observable(""); + + //graph related things + + this.expanded = ko.observable(true); + this.keepExpanded = ko.observable(false); + this.collapsed = ko.observable(true); + this.peek = ko.observable(false); + + this.color = ko.observable(Utils.getColorForNode(category)); + this.drawOrderHint = ko.observable(0); + this.radius = ko.observable(0); } getId = () : string => { - return this._id; + return this._id(); } setId = (id: string) : void => { - this._id = id; + this._id(id); } getKey = () : number => { @@ -164,9 +150,9 @@ export class Node { } } - getGraphNodeId = () :string => { - const x = Math.abs(this.getKey())-1 - return 'node'+x + // TODO: remove/comment-out if unused + getGraphNodeId = () : string => { + return 'node' + (Math.abs(this.getKey())-1).toString(); } getName = () : string => { @@ -212,64 +198,60 @@ export class Node { } getPosition = () : {x:number, y:number} => { - return {x: this.x, y: this.y}; + return {x: this.x(), y: this.y()}; } - getRealPosition = () : {x:number, y:number} => { - return {x: this.realX, y: this.realY}; - } + // getRealPosition = () : {x:number, y:number} => { + // return {x: this.realX, y: this.realY}; + // } setPosition = (x: number, y: number, allowSnap: boolean = true) : void => { - this.realX = x; - this.realY = y; - - if (Eagle.getInstance().snapToGrid() && allowSnap){ - this.x = Utils.snapToGrid(this.realX, this.getDisplayWidth()); - this.y = Utils.snapToGrid(this.realY, this.getDisplayHeight()); - } else { - this.x = this.realX; - this.y = this.realY; - } - } - - changePosition = (dx : number, dy : number, allowSnap: boolean = true) : {dx:number, dy:number} => { - this.realX += dx; - this.realY += dy; - - const beforePos = {x:this.x, y:this.y}; - - if (Eagle.getInstance().snapToGrid() && allowSnap){ - this.x = Utils.snapToGrid(this.realX, this.getDisplayWidth()); - this.y = Utils.snapToGrid(this.realY, this.getDisplayHeight()); - - return {dx:this.x - beforePos.x, dy:this.y - beforePos.y}; - } else { - this.x = this.realX; - this.y = this.realY; + // this.realX = x; + // this.realY = y; + + // if (Eagle.getInstance().snapToGrid() && allowSnap){ + // this.x(Utils.snapToGrid(this.realX, this.getDisplayRadius())); + // this.y(Utils.snapToGrid(this.realY, this.getDisplayRadius())); + // } else { + // this.x(this.realX); + // this.y(this.realY); + // } + this.x(x) + this.y(y) + } + + changePosition = (dx : number, dy : number, allowSnap: boolean = true) : void => { + // this.realX += dx; + // this.realY += dy; + + // const beforePos = {x:this.x(), y:this.y()}; + + // if (Eagle.getInstance().snapToGrid() && allowSnap){ + // this.x(Utils.snapToGrid(this.realX, this.getDisplayRadius())); + // this.y(Utils.snapToGrid(this.realY, this.getDisplayRadius())); + + // return {dx:this.x() - beforePos.x, dy:this.y() - beforePos.y}; + // } else { + // this.x(this.realX); + // this.y(this.realY); - return {dx:dx, dy:dy}; - } - } - - resetReal = () : void => { - this.realX = this.x; - this.realY = this.y; + // return {dx:dx, dy:dy}; + // } + this.x(this.x()+dx) + this.y(this.y()+dy) } - getWidth = () : number => { - return this.width; - } - - setWidth = (width : number) : void => { - this.width = width; - } + // resetReal = () : void => { + // this.realX = this.x(); + // this.realY = this.y(); + // } - getHeight = () : number => { - return this.height; + getRadius = () : number => { + return this.radius(); } - setHeight = (height : number) : void => { - this.height = height; + setRadius = (radius : number) : void => { + this.radius(radius); } getColor = () : string => { @@ -357,23 +339,15 @@ export class Node { } isPeek = () : boolean => { - return this.peek; + return this.peek(); } setPeek = (value : boolean) : void => { - this.peek = value; + this.peek(value); } - isFlipPorts = () : boolean => { - return this.flipPorts(); - } - - setFlipPorts = (value : boolean) : void => { - this.flipPorts(value); - } - - toggleFlipPorts = () : void => { - this.flipPorts(!this.flipPorts()); + togglePeek = () : void => { + this.setPeek(!this.peek()); } isLocked : ko.PureComputed = ko.pureComputed(() => { @@ -410,6 +384,21 @@ export class Node { return result; } + getPorts = () : Field[] => { + const results: Field[] = this.getInputPorts() + this.getOutputPorts().forEach(function(outputPort){ + for (const result of results){ + if(result.getId() === outputPort.getId()){ + continue + }else{ + results.push(outputPort) + } + } + }) + + return results; + } + getInputApplicationInputPorts = () : Field[] => { if (this.inputApplication() === null){ return []; @@ -473,6 +462,16 @@ export class Node { return null; } + getFieldById = (id : string) : Field | null => { + for (const field of this.fields()){ + if (field.getId() === id){ + return field; + } + } + + return null; + } + hasFieldWithDisplayText = (displayText : string) : boolean => { for (const field of this.fields()){ if (field.getDisplayText() === displayText){ @@ -581,6 +580,10 @@ export class Node { return this.categoryType(); } + setCategoryType = (categoryType: Category.Type) : void => { + this.categoryType(categoryType); + } + setRepositoryUrl = (url: string) : void => { this.repositoryUrl(url); } @@ -617,6 +620,18 @@ export class Node { return this.category() === Category.Scatter; } + isExclusiveForceNode = () : boolean => { + return this.category() === Category.ExclusiveForceNode; + } + + isComment = () : boolean => { + return this.category() === Category.Comment; + } + + isDescription = () : boolean => { + return this.category() === Category.Description; + } + isGather = () : boolean => { return this.category() === Category.Gather; } @@ -637,12 +652,8 @@ export class Node { return this.category() === Category.Service; } - isResizable = () : boolean => { - return CategoryData.getCategoryData(this.category()).isResizable; - } - isGroup = () : boolean => { - return CategoryData.getCategoryData(this.category()).canContainComponents; + return CategoryData.getCategoryData(this.category()).isGroup; } canHaveInputs = () : boolean => { @@ -768,14 +779,13 @@ export class Node { } clear = () : void => { - this._id = ""; + this._id(""); this.key(0); this.name(""); this.description(""); - this.x = 0; - this.y = 0; - this.width = Node.DEFAULT_WIDTH; - this.height = Node.DEFAULT_HEIGHT; + this.x(0); + this.y(0); + this.radius(GraphConfig.MINIMUM_CONSTRUCT_RADIUS); this.color(Node.DEFAULT_COLOR); this.drawOrderHint(0); @@ -802,59 +812,22 @@ export class Node { this.dataHash(""); } - getDisplayWidth = () : number => { + getDisplayRadius = () : number => { if (this.isGroup() && this.isCollapsed()){ - return Node.GROUP_COLLAPSED_WIDTH; + return GraphConfig.MINIMUM_CONSTRUCT_RADIUS; } if (!this.isGroup() && !this.isCollapsed()){ - return this.width; + return this.radius(); } + /* if (this.isData() && !this.isCollapsed() && !this.isPeek()){ return Node.DATA_COMPONENT_WIDTH; } + */ - return this.width; - } - - getDisplayHeight = () : number => { - if (this.isResizable()){ - if (this.isCollapsed()){ - return Node.GROUP_COLLAPSED_HEIGHT; - } else { - return this.height; - } - } - - if (!this.isGroup() && this.isCollapsed() && !this.isPeek()){ - return 32; - } - - if (this.isData() && this.isCollapsed() && !this.isPeek()){ - return Node.DATA_COMPONENT_HEIGHT; - } - - if (this.getCategory() === Category.Service){ - // NOTE: Service nodes can't have input ports, or input application output ports! - return (2 * 30) + - (this.getInputApplicationInputPorts().length * 24) + - (this.getInputApplicationOutputPorts().length * 24) + - 8; - } - - const leftHeight = ( - this.getInputPorts().length + - this.getInputApplicationInputPorts().length + - this.getInputApplicationOutputPorts().length + - 2) * 24; - const rightHeight = ( - this.getOutputPorts().length + - this.getOutputApplicationInputPorts().length + - this.getOutputApplicationOutputPorts().length + - 2) * 24; - - return Math.max(leftHeight, rightHeight); + return this.radius(); } getGitHTML : ko.PureComputed = ko.pureComputed(() => { @@ -1134,30 +1107,42 @@ export class Node { } } + // removes all InputPort ports, and changes all InputOutput ports to be OutputPort removeAllInputPorts = () : void => { for (let i = this.fields().length - 1 ; i >= 0 ; i--){ - if (this.fields()[i].getUsage() === Daliuge.FieldUsage.InputPort){ + const field: Field = this.fields()[i]; + + if (field.getUsage() === Daliuge.FieldUsage.InputPort){ this.fields.splice(i, 1); } + if (field.getUsage() === Daliuge.FieldUsage.InputOutput){ + field.setUsage(Daliuge.FieldUsage.OutputPort); + } } } + // removes all OutputPort ports, and changes all InputOutput ports to be InputPort removeAllOutputPorts = () : void => { for (let i = this.fields().length - 1 ; i >= 0 ; i--){ - if (this.fields()[i].getUsage() === Daliuge.FieldUsage.OutputPort){ + const field: Field = this.fields()[i]; + + if (field.getUsage() === Daliuge.FieldUsage.OutputPort){ this.fields.splice(i, 1); } + if (field.getUsage() === Daliuge.FieldUsage.InputOutput){ + field.setUsage(Daliuge.FieldUsage.InputPort); + } } } clone = () : Node => { + const result : Node = new Node(this.key(), this.name(), this.description(), this.category()); - result._id = this._id; - result.x = this.x; - result.y = this.y; - result.width = this.width; - result.height = this.height; + result._id(this._id()); + result.x(this.x()); + result.y(this.y()); + result.radius(this.radius()); result.categoryType(this.categoryType()); result.color(this.color()); result.drawOrderHint(this.drawOrderHint()); @@ -1169,22 +1154,9 @@ export class Node { result.expanded(this.expanded()); result.keepExpanded(this.expanded()); - result.peek = this.peek; - result.flipPorts(this.flipPorts()); - - // copy input,output and exit applications - if (this.inputApplication() === null){ - result.inputApplication(null); - } else { - result.inputApplication(this.inputApplication().clone()); - } - if (this.outputApplication() === null){ - result.outputApplication(null); - } else { - result.outputApplication(this.outputApplication().clone()); - } + result.peek(this.peek()); - result.subject = this.subject; + result.subject(this.subject()); // clone fields for (const field of this.fields()){ @@ -1195,12 +1167,19 @@ export class Node { result.commitHash(this.commitHash()); result.paletteDownloadUrl(this.paletteDownloadUrl()); result.dataHash(this.dataHash()); + + if (this.hasInputApplication()){ + result.inputApplication(this.inputApplication().clone()); + } + if (this.hasOutputApplication()){ + result.outputApplication(this.outputApplication().clone()); + } return result; } - getErrorsWarnings = (eagle: Eagle): Errors.ErrorsWarnings => { - const result: {warnings: Errors.Issue[], errors: Errors.Issue[]} = {warnings: [], errors: []}; + getErrorsWarnings = (eagle: Eagle): ActionMessage[] => { + const result: ActionMessage[] = []; Node.isValid(eagle, this, Eagle.selectedLocation(), false, false, result); @@ -1327,14 +1306,6 @@ export class Node { node0.getCommitHash() === node1.getCommitHash(); } - static requiresUpdate = (node0: Node, node1: Node) : boolean => { - return node0.getRepositoryUrl() !== "" && - node1.getRepositoryUrl() !== "" && - node0.getRepositoryUrl() === node1.getRepositoryUrl() && - node0.getName() === node1.getName() && - node0.getCommitHash() !== node1.getCommitHash(); - } - static canHaveInputApp = (node : Node) : boolean => { return CategoryData.getCategoryData(node.getCategory()).canHaveInputApplication; } @@ -1343,7 +1314,7 @@ export class Node { return CategoryData.getCategoryData(node.getCategory()).canHaveOutputApplication; } - static fromOJSJson = (nodeData : any, errorsWarnings: Errors.ErrorsWarnings, isPaletteNode: boolean, generateKeyFunc: () => number) : Node => { + static fromOJSJson = (nodeData : any, errors: ActionMessage[], isPaletteNode: boolean, generateKeyFunc: () => number) : Node => { let name = ""; if (typeof nodeData.name !== 'undefined'){ name = nodeData.name; @@ -1351,7 +1322,7 @@ export class Node { if (typeof nodeData.text !== 'undefined'){ name = nodeData.text; } else { - errorsWarnings.errors.push(Errors.Message("Node " + nodeData.key + " has undefined text and name " + nodeData + "!")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node " + nodeData.key + " has undefined text and name" + nodeData + "!")); } } @@ -1380,7 +1351,7 @@ export class Node { // if category is not known, then add error if (!Utils.isKnownCategory(category)){ - errorsWarnings.errors.push(Errors.Message("Node with name " + name + " has unknown category: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node with name " + name + " has unknown category: " + category)); category = Category.Unknown; } @@ -1397,9 +1368,13 @@ export class Node { node.description(nodeData.description); } + if(!isPaletteNode && nodeData.radius === undefined){ + GraphRenderer.legacyGraph = true + } + // get size (if exists) - let width = Node.DEFAULT_WIDTH; - let height = Node.DEFAULT_HEIGHT; + let width = GraphConfig.NORMAL_NODE_RADIUS; + let height = GraphConfig.NORMAL_NODE_RADIUS; if (typeof nodeData.desiredSize !== 'undefined'){ width = nodeData.desiredSize.width; height = nodeData.desiredSize.height; @@ -1410,18 +1385,15 @@ export class Node { if (typeof nodeData.height !== 'undefined'){ height = nodeData.height; } - node.width = width; - node.height = height; - - // if node is not a group or comment/description, make its width/height the default values - if (!CategoryData.getCategoryData(node.getCategory()).isResizable){ - node.width = Node.DEFAULT_WIDTH; - node.height = Node.DEFAULT_HEIGHT; - } - // flipPorts - if (typeof nodeData.flipPorts !== 'undefined'){ - node.flipPorts(nodeData.flipPorts); + if (node.isGroup()){ + node.radius(Math.max(width, height)); + } else { + if (node.isBranch()){ + node.radius(GraphConfig.BRANCH_NODE_RADIUS); + } else { + node.radius(GraphConfig.NORMAL_NODE_RADIUS); + } } // expanded @@ -1483,7 +1455,7 @@ export class Node { // debug //console.log("node", nodeData.text); - //console.log("inputAppName", nodeData.inputAppName, "inputApplicationName", nodeData.inputApplicationName, "inpuApplication", nodeData.inputApplication, "inputApplicationType", nodeData.inputApplicationType); + //console.log("inputAppName", nodeData.inputAppName, "inputApplicationName", nodeData.inputApplicationName, "inputApplication", nodeData.inputApplication, "inputApplicationType", nodeData.inputApplicationType); //console.log("outputAppName", nodeData.outputAppName, "outputApplicationName", nodeData.outputApplicationName, "outputApplication", nodeData.outputApplication, "outputApplicationType", nodeData.outputApplicationType); // these next six if statements are covering old versions of nodes, that @@ -1491,51 +1463,51 @@ export class Node { // NOTE: the key for the new nodes are not set correctly, they will have to be overwritten later if (inputApplicationName !== ""){ if (!CategoryData.getCategoryData(category).canHaveInputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication to unsuitable node: " + category)); } else { // check applicationType is an application if (CategoryData.getCategoryData(inputApplicationType).categoryType === Category.Type.Application){ node.inputApplication(Node.createEmbeddedApplicationNode(inputApplicationKey, inputApplicationName, inputApplicationType, inputApplicationDescription, node.getKey())); } else { - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication of unsuitable type: " + inputApplicationType + ", to node.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication of unsuitable type: " + inputApplicationType + ", to node.")); } } } if (inputApplicationName !== "" && inputApplicationType !== Category.None){ if (!CategoryData.getCategoryData(category).canHaveInputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication to unsuitable node: " + category)); } else { // check applicationType is an application if (CategoryData.getCategoryData(inputApplicationType).categoryType === Category.Type.Application){ node.inputApplication(Node.createEmbeddedApplicationNode(inputApplicationKey, inputApplicationName, inputApplicationType, inputApplicationDescription, node.getKey())); } else { - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication of unsuitable type: " + inputApplicationType + ", to node.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication of unsuitable type: " + inputApplicationType + ", to node.")); } } } if (outputApplicationName !== ""){ if (!CategoryData.getCategoryData(category).canHaveOutputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add outputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add outputApplication to unsuitable node: " + category)); } else { // check applicationType is an application if (CategoryData.getCategoryData(outputApplicationType).categoryType === Category.Type.Application){ node.outputApplication(Node.createEmbeddedApplicationNode(outputApplicationKey, outputApplicationName, outputApplicationType, outputApplicationDescription, node.getKey())); } else { - errorsWarnings.errors.push(Errors.Message("Attempt to add outputApplication of unsuitable type: " + outputApplicationType + ", to node.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add outputApplication of unsuitable type: " + outputApplicationType + ", to node.")); } } } if (outputApplicationName !== "" && outputApplicationType !== Category.None){ if (!CategoryData.getCategoryData(category).canHaveOutputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add outputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add outputApplication to unsuitable node: " + category)); } else { if (CategoryData.getCategoryData(outputApplicationType).categoryType === Category.Type.Application){ node.outputApplication(Node.createEmbeddedApplicationNode(outputApplicationKey, outputApplicationName, outputApplicationType, outputApplicationDescription, node.getKey())); } else { - errorsWarnings.errors.push(Errors.Message("Attempt to add outputApplication of unsuitable type: " + outputApplicationType + ", to node.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add outputApplication of unsuitable type: " + outputApplicationType + ", to node.")); } } } @@ -1552,15 +1524,15 @@ export class Node { // debug hack for *really* old nodes that just use 'application' to specify the inputApplication if (nodeData.application !== undefined && nodeData.application !== ""){ - errorsWarnings.errors.push(Errors.Message("Only found old application type, not new input application type and output application type: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Only found old application type, not new input application type and output application type: " + category)); if (!CategoryData.getCategoryData(category).canHaveInputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication to unsuitable node: " + category)); } else { if (CategoryData.getCategoryData(category).categoryType === Category.Type.Application){ node.inputApplication(Node.createEmbeddedApplicationNode(null, nodeData.application, category, "", node.getKey())); } else { - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication of unsuitable type: " + category + ", to node.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication of unsuitable type: " + category + ", to node.")); } } } @@ -1568,17 +1540,17 @@ export class Node { // read the 'real' input and output apps, correctly specified as nested nodes if (typeof nodeData.inputApplication !== 'undefined' && nodeData.inputApplication !== null){ if (!CategoryData.getCategoryData(category).canHaveInputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add inputApplication to unsuitable node: " + category)); } else { - node.inputApplication(Node.fromOJSJson(nodeData.inputApplication, errorsWarnings, isPaletteNode, generateKeyFunc)); + node.inputApplication(Node.fromOJSJson(nodeData.inputApplication, errors, isPaletteNode, generateKeyFunc)); node.inputApplication().setEmbedKey(node.getKey()); } } if (typeof nodeData.outputApplication !== 'undefined' && nodeData.outputApplication !== null){ if (!CategoryData.getCategoryData(category).canHaveOutputApplication){ - errorsWarnings.errors.push(Errors.Message("Attempt to add outputApplication to unsuitable node: " + category)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Attempt to add outputApplication to unsuitable node: " + category)); } else { - node.outputApplication(Node.fromOJSJson(nodeData.outputApplication, errorsWarnings, isPaletteNode, generateKeyFunc)); + node.outputApplication(Node.fromOJSJson(nodeData.outputApplication, errors, isPaletteNode, generateKeyFunc)); node.outputApplication().setEmbedKey(node.getKey()); } } @@ -1675,7 +1647,7 @@ export class Node { const field = Field.fromOJSJson(fieldData); node.inputApplication().addField(field); } else { - errorsWarnings.errors.push(Errors.Message("Can't add input app field " + fieldData.text + " to node " + node.getName() + ". No input application.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Can't add input app field " + fieldData.text + " to node " + node.getName() + ". No input application.")); } } } @@ -1687,7 +1659,7 @@ export class Node { const field = Field.fromOJSJson(fieldData); node.outputApplication().addField(field); } else { - errorsWarnings.errors.push(Errors.Message("Can't add output app field " + fieldData.text + " to node " + node.getName() + ". No output application.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Can't add output app field " + fieldData.text + " to node " + node.getName() + ". No output application.")); } } } @@ -1702,7 +1674,7 @@ export class Node { if (node.canHaveInputs()){ node.addField(port); } else { - Node.addPortToEmbeddedApplication(node, port, true, errorsWarnings, generateKeyFunc); + Node.addPortToEmbeddedApplication(node, port, true, errors, generateKeyFunc); } } } @@ -1717,7 +1689,7 @@ export class Node { if (node.canHaveOutputs()){ node.addField(port); } else { - Node.addPortToEmbeddedApplication(node, port, false, errorsWarnings, generateKeyFunc); + Node.addPortToEmbeddedApplication(node, port, false, errors, generateKeyFunc); } } } @@ -1732,7 +1704,7 @@ export class Node { node.inputApplication().addField(port); } else { - errorsWarnings.errors.push(Errors.Message("Can't add inputLocal port " + inputLocalPort.IdText + " to node " + node.getName() + ". No input application.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Can't add inputLocal port " + inputLocalPort.IdText + " to node " + node.getName() + ". No input application.")); } } } @@ -1747,7 +1719,7 @@ export class Node { if (node.hasOutputApplication()){ node.outputApplication().addField(port); } else { - errorsWarnings.errors.push(Errors.Message("Can't add outputLocal port " + outputLocalPort.IdText + " to node " + node.getName() + ". No output application.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Can't add outputLocal port " + outputLocalPort.IdText + " to node " + node.getName() + ". No output application.")); } } } @@ -1775,34 +1747,34 @@ export class Node { } } - private static addPortToEmbeddedApplication(node: Node, port: Field, input: boolean, errorsWarnings: Errors.ErrorsWarnings, generateKeyFunc: () => number){ + private static addPortToEmbeddedApplication(node: Node, port: Field, input: boolean, errors: ActionMessage[], generateKeyFunc: () => number){ // check that the node already has an appropriate embedded application, otherwise create it if (input){ if (!node.hasInputApplication()){ if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS)){ node.inputApplication(Node.createEmbeddedApplicationNode(generateKeyFunc(), port.getDisplayText(), Category.UnknownApplication, "", node.getKey())); - errorsWarnings.errors.push(Errors.Message("Created new embedded input application (" + node.inputApplication().getName() + ") for node (" + node.getName() + ", " + node.getKey() + "). Application category is " + node.inputApplication().getCategory() + " and may require user intervention.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Created new embedded input application (" + node.inputApplication().getName() + ") for node (" + node.getName() + ", " + node.getKey() + "). Application category is " + node.inputApplication().getCategory() + " and may require user intervention.")); } else { - errorsWarnings.errors.push(Errors.Message("Cannot add input port to construct that doesn't support input ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Cannot add input port to construct that doesn't support input ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); return; } } node.inputApplication().addField(port); - errorsWarnings.warnings.push(Errors.Message("Moved input port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ", " + node.getKey() + ") to an embedded input application (" + node.inputApplication().getName() + ", " + node.inputApplication().getKey() + ")")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Moved input port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ", " + node.getKey() + ") to an embedded input application (" + node.inputApplication().getName() + ", " + node.inputApplication().getKey() + ")")); } else { // determine whether we should check (and possibly add) an output or exit application, depending on the type of this node if (node.canHaveOutputApplication()){ if (!node.hasOutputApplication()){ if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS)){ node.outputApplication(Node.createEmbeddedApplicationNode(generateKeyFunc(), port.getDisplayText(), Category.UnknownApplication, "", node.getKey())); - errorsWarnings.errors.push(Errors.Message("Created new embedded output application (" + node.outputApplication().getName() + ") for node (" + node.getName() + ", " + node.getKey() + "). Application category is " + node.outputApplication().getCategory() + " and may require user intervention.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Created new embedded output application (" + node.outputApplication().getName() + ") for node (" + node.getName() + ", " + node.getKey() + "). Application category is " + node.outputApplication().getCategory() + " and may require user intervention.")); } else { - errorsWarnings.errors.push(Errors.Message("Cannot add output port to construct that doesn't support output ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Cannot add output port to construct that doesn't support output ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); return; } } node.outputApplication().addField(port); - errorsWarnings.warnings.push(Errors.Message("Moved output port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ", " + node.getKey() + ") to an embedded output application (" + node.outputApplication().getName() + ", " + node.outputApplication().getKey() + ")")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Moved output port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ", " + node.getKey() + ") to an embedded output application (" + node.outputApplication().getName() + ", " + node.outputApplication().getKey() + ")")); } else { // if possible, add port to output side of input application if (node.canHaveInputApplication()){ @@ -1810,14 +1782,14 @@ export class Node { if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS)){ node.inputApplication(Node.createEmbeddedApplicationNode(generateKeyFunc(), port.getDisplayText(), Category.UnknownApplication, "", node.getKey())); } else { - errorsWarnings.errors.push(Errors.Message("Cannot add input port to construct that doesn't support input ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Cannot add input port to construct that doesn't support input ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); return; } } node.inputApplication().addField(port); - errorsWarnings.warnings.push(Errors.Message("Moved output port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + "," + node.getKey() + ") to output of the embedded input application")); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Moved output port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + "," + node.getKey() + ") to output of the embedded input application")); } else { - errorsWarnings.errors.push(Errors.Message("Can't add port to embedded application. Node can't have output OR exit application.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Can't add port to embedded application. Node can't have output OR exit application.")); } } } @@ -1908,12 +1880,10 @@ export class Node { result.key = node.key(); result.name = node.name(); result.description = node.description(); - result.x = node.x; - result.y = node.y; - result.width = node.width; - result.height = node.height; + result.x = node.x(); + result.y = node.y(); + result.radius = node.radius(); result.collapsed = node.collapsed(); - result.flipPorts = node.flipPorts(); result.subject = node.subject(); result.expanded = node.expanded(); result.repositoryUrl = node.repositoryUrl(); @@ -1979,207 +1949,167 @@ export class Node { return result; } - /* - // display/visualisation data - static toV3NodeJson = (node : Node, index : number) : object => { - const result : any = {}; - - result.categoryType = node.categoryType(); - result.componentKey = index.toString(); - - result.color = node.color(); - result.drawOrderHint = node.drawOrderHint(); - - result.x = node.x; - result.y = node.y; - result.width = node.width; - result.height = node.height; - result.collapsed = node.collapsed(); - result.flipPorts = node.flipPorts(); - - result.expanded = node.expanded(); - - result.repositoryUrl = node.repositoryUrl(); - result.commitHash = node.commitHash(); - result.paletteDownloadUrl = node.paletteDownloadUrl(); - result.dataHash = node.dataHash(); + static createEmbeddedApplicationNode = (key: number, name : string, category: Category, description: string, embedKey: number) : Node => { + console.assert(CategoryData.getCategoryData(category).categoryType === Category.Type.Application); - return result; + const node = new Node(key, name, description, category); + node.setEmbedKey(embedKey); + node.setRadius(GraphConfig.NORMAL_NODE_RADIUS); + return node; } - static fromV3NodeJson = (nodeData : any, key: string, errorsWarnings: Errors.ErrorsWarnings) : Node => { - const result = new Node(parseInt(key, 10), "", "", Category.Unknown); - - result.categoryType(nodeData.categoryType); - result.color(nodeData.color); - result.drawOrderHint(nodeData.drawOrderHint); - - result.x = nodeData.x; - result.y = nodeData.y; - result.width = nodeData.width; - result.height = nodeData.height; - result.collapsed(nodeData.collapsed); - result.flipPorts(nodeData.flipPorts); + getInputAppText = () : string => { + if (!Node.canHaveInputApp(this)){ + return ""; + } - result.expanded(nodeData.expanded); + const inputApplication : Node = this.getInputApplication(); - result.repositoryUrl(nodeData.repositoryUrl); - result.commitHash(nodeData.commitHash); - result.paletteDownloadUrl(nodeData.paletteDownloadUrl); - result.dataHash(nodeData.dataHash); + if (typeof inputApplication === "undefined" || inputApplication === null){ + return Node.NO_APP_STRING; + } - return result; + return inputApplication.getName() === "" ? Node.NO_APP_NAME_STRING : inputApplication.getName(); } - */ - - // graph data - // "name" and "description" are considered part of the structure of the graph, it would be hard to add them to the display part (parameters would have to be treated the same way) - /* - static toV3ComponentJson = (node : Node) : object => { - const result : any = {}; - const useNewCategories : boolean = Setting.findValue(Utils.TRANSLATE_WITH_NEW_CATEGORIES); - result.category = useNewCategories ? GraphUpdater.translateNewCategory(node.category()) : node.category(); + getOutputAppText = () : string => { + if (!Node.canHaveOutputApp(this)){ + return ""; + } - result.name = node.name(); - result.description = node.description(); + const outputApplication : Node = this.getOutputApplication(); - result.streaming = node.streaming(); - result.precious = node.precious(); - result.subject = node.subject(); // TODO: not sure if this should be here or in Node JSON + if (typeof outputApplication === "undefined" || outputApplication === null){ + return Node.NO_APP_STRING; + } + return outputApplication.getName() === "" ? Node.NO_APP_NAME_STRING : outputApplication.getName() + } - result.parentKey = node.parentKey(); - result.embedKey = node.embedKey(); + getInputAppColor = () : string => { + if (!Node.canHaveInputApp(this)){ + return "white"; + } - result.inputApplicationKey = -1; - result.outputApplicationKey = -1; + const inputApplication : Node = this.getInputApplication(); - // add input ports - result.inputPorts = {}; - for (const inputPort of node.getInputPorts()){ - result.inputPorts[inputPort.getId()] = Port.toV3Json(inputPort); + if (typeof inputApplication === "undefined" || inputApplication === null){ + return "white"; } - // add output ports - result.outputPorts = {}; - for (const outputPort of node.getOutputPorts()){ - result.outputPorts[outputPort.getId()] = Port.toV3Json(outputPort); - } + return inputApplication.getColor(); + } - // add component parameters - result.componentParameters = {}; - for (let i = 0 ; i < node.fields().length ; i++){ - const field = node.fields()[i]; - result.componentParameters[i] = Field.toV3Json(field); + getOutputAppColor = () : string => { + if (!Node.canHaveOutputApp(this)){ + return "white"; } - // add Application Arguments - result.applicationParameters = {}; - for (let i = 0 ; i < node.applicationArgs().length ; i++){ - const field = node.applicationArgs()[i]; - result.applicationParameters[i] = Field.toV3Json(field); + const outputApplication : Node = this.getOutputApplication(); + + if (typeof outputApplication === "undefined" || outputApplication === null){ + return "white"; } - return result; + return outputApplication.getColor(); } - */ - - /* - static fromV3ComponentJson = (nodeData: any, node: Node, errors: Eagle.ErrorsWarnings): void => { - node.category(nodeData.category); - node.name(nodeData.name); - node.description(nodeData.description); - node.streaming(nodeData.streaming); - node.precious(nodeData.precious); - node.subject(nodeData.subject); - - node.parentKey(nodeData.parentKey); - node.embedKey(nodeData.embedKey); + getDataIcon = () : string => { + switch (this.getCategory()){ + case Category.File: + return "/static/assets/svg/hard-drive.svg"; + case Category.Memory: + return "/static/assets/svg/memory.svg"; + case Category.S3: + return "/static/assets/svg/s3_bucket.svg"; + case Category.NGAS: + return "/static/assets/svg/ngas.svg"; + case Category.Plasma: + return "/static/assets/svg/plasma.svg"; + default: + console.warn("No icon available for node category", this.getCategory()); + return ""; + } } - */ - - static createEmbeddedApplicationNode = (key: number, name : string, category: Category, description: string, embedKey: number) : Node => { - console.assert(CategoryData.getCategoryData(category).categoryType === Category.Type.Application); - const node = new Node(key, name, description, category); - node.setEmbedKey(embedKey); - return node; - } + static isValid = (eagle: Eagle, node: Node, selectedLocation: Eagle.FileType, showNotification : boolean, showConsole : boolean, errors: ActionMessage[]) : Eagle.LinkValid => { + // check that node has modern (not legacy) category + if (node.getCategory() === Category.Component){ + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has legacy category (" + node.getCategory() + ")", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixNodeCategory(eagle, node, Category.PythonApp, Category.Type.Application)}, ""); + errors.push(issue); + } - static isValid = (eagle: Eagle, node: Node, selectedLocation: Eagle.FileType, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : Eagle.LinkValid => { // check that all port dataTypes have been defined for (const port of node.getInputPorts()){ if (port.isType(Daliuge.DataType.Unknown)){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has input port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(eagle, port)}, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has input port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(port)}, ""); + errors.push(issue); } } for (const port of node.getOutputPorts()){ if (port.isType(Daliuge.DataType.Unknown)){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has output port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(eagle, port)}, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has output port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(port)}, ""); + errors.push(issue); } } for (const port of node.getInputApplicationInputPorts()){ if (port.isType(Daliuge.DataType.Unknown)){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has input application (" + node.getInputApplication().getName() + ") with input port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(eagle, port)}, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has input application (" + node.getInputApplication().getName() + ") with input port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(port)}, ""); + errors.push(issue); } } for (const port of node.getInputApplicationOutputPorts()){ if (port.isType(Daliuge.DataType.Unknown)){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has input application (" + node.getInputApplication().getName() + ") with output port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(eagle, port)}, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has input application (" + node.getInputApplication().getName() + ") with output port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(port)}, ""); + errors.push(issue); } } for (const port of node.getOutputApplicationInputPorts()){ if (port.isType(Daliuge.DataType.Unknown)){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has output application (" + node.getOutputApplication().getName() + ") with input port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(eagle, port)}, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has output application (" + node.getOutputApplication().getName() + ") with input port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(port)}, ""); + errors.push(issue); } } for (const port of node.getOutputApplicationOutputPorts()){ if (port.isType(Daliuge.DataType.Unknown)){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has output application (" + node.getOutputApplication().getName() + ") with output port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(eagle, port)}, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has output application (" + node.getOutputApplication().getName() + ") with output port (" + port.getDisplayText() + ") whose type is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldType(port)}, ""); + errors.push(issue); } } // check that all fields have ids for (const field of node.getFields()){ if (field.getId() === "" || field.getId() === null){ - const issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has field (" + field.getDisplayText() + ") with no id", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldId(eagle, field)}, "Generate id for field"); - errorsWarnings.errors.push(issue); + const issue = ActionMessage.ShowFix(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") has field (" + field.getDisplayText() + ") with no id", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldId(field)}, "Generate id for field"); + errors.push(issue); } } // check that all fields have default values for (const field of node.getFields()){ if (field.getDefaultValue() === "" && !field.isType(Daliuge.DataType.String) && !field.isType(Daliuge.DataType.Password) && !field.isType(Daliuge.DataType.Object) && !field.isType(Daliuge.DataType.Unknown)) { - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has a component parameter (" + field.getDisplayText() + ") whose default value is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, function(){Utils.fixFieldDefaultValue(eagle, field)}, "Generate default value for parameter"); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has a component parameter (" + field.getDisplayText() + ") whose default value is not specified", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, function(){Utils.fixFieldDefaultValue(field)}, "Generate default value for parameter"); + errors.push(issue); } } // check that all fields have known types for (const field of node.getFields()){ if (!Utils.validateType(field.getType())) { - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has a component parameter (" + field.getDisplayText() + ") whose type (" + field.getType() + ") is unknown", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, function(){Utils.fixFieldType(eagle, field)}, "Prepend existing type (" + field.getType() + ") with 'Object.'"); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has a component parameter (" + field.getDisplayText() + ") whose type (" + field.getType() + ") is unknown", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, function(){Utils.fixFieldType(field)}, "Prepend existing type (" + field.getType() + ") with 'Object.'"); + errors.push(issue); } } // check that all fields "key" attribute is the same as the key of the node they belong to for (const field of node.getFields()){ if (field.getNodeKey() !== node.getKey()) { - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has a field (" + field.getDisplayText() + ") whose key (" + field.getNodeKey() + ") doesn't match the node (" + node.getKey() + ")", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, function(){Utils.fixFieldKey(eagle, node, field)}, "Set field node key correctly"); - errorsWarnings.errors.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") has a field (" + field.getDisplayText() + ") whose key (" + field.getNodeKey() + ") doesn't match the node (" + node.getKey() + ")", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, function(){Utils.fixFieldKey(eagle, node, field)}, "Set field node key correctly"); + errors.push(issue); } } @@ -2191,11 +2121,11 @@ export class Node { const field1 = node.getFields()[j]; if (i !== j && field0.getDisplayText() === field1.getDisplayText() && field0.getParameterType() === field1.getParameterType()){ if (field0.getId() === field1.getId()){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has multiple attributes with the same display text (" + field0.getDisplayText() + ").", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixNodeMergeFieldsByIndex(eagle, node, i, j)}, "Merge fields"); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has multiple attributes with the same id text (" + field0.getDisplayText() + ").", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixNodeMergeFieldsByIndex(eagle, selectedLocation, node, i, j)}, "Merge fields"); + errors.push(issue); } else { - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has multiple attributes with the same display text (" + field0.getDisplayText() + ").", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixNodeMergeFields(eagle, node, field0, field1)}, "Merge fields"); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has multiple attributes with the same id text (" + field0.getDisplayText() + ").", function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixNodeMergeFields(eagle, selectedLocation, node, field0, field1)}, "Merge fields"); + errors.push(issue); } } } @@ -2230,8 +2160,8 @@ export class Node { } const message = "Node " + node.getKey() + " (" + node.getName() + ") with category " + node.getCategory() + " contains field (" + field.getDisplayText() + ") with unsuitable type (" + field.getParameterType() + ")."; - const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldParameterType(eagle, node, field, suitableType)}, "Switch to suitable type, or remove if no suitable type"); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, message, function(){Utils.showNode(eagle, selectedLocation, node.getId());}, function(){Utils.fixFieldParameterType(eagle, node, field, suitableType)}, "Switch to suitable type, or remove if no suitable type"); + errors.push(issue); } } @@ -2243,60 +2173,62 @@ export class Node { const maxOutputs = cData.maxOutputs; if (node.getInputPorts().length < minInputs){ - errorsWarnings.warnings.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") may have too few input ports. A " + node.getCategory() + " component would typically have at least " + minInputs)); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") may have too few input ports. A " + node.getCategory() + " component would typically have at least " + minInputs)); } if (node.getInputPorts().length > maxInputs){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has too many input ports. Should have at most " + maxInputs)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") has too many input ports. Should have at most " + maxInputs)); } if (node.getOutputPorts().length < minOutputs){ - errorsWarnings.warnings.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") may have too few output ports. A " + node.getCategory() + " component would typically have at least " + minOutputs)); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") may have too few output ports. A " + node.getCategory() + " component would typically have at least " + minOutputs)); } if (node.getOutputPorts().length > maxOutputs){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") may have too many output ports. Should have at most " + maxOutputs)); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") may have too many output ports. Should have at most " + maxOutputs)); } // check that all nodes should have at least one connected edge, otherwise what purpose do they serve? let isConnected: boolean = false; - for (const edge of eagle.logicalGraph().getEdges()){ - if (edge.getSrcNodeKey() === node.getKey() || edge.getDestNodeKey() === node.getKey()){ - isConnected = true; - break; + if (selectedLocation === Eagle.FileType.Graph){ + for (const edge of eagle.logicalGraph().getEdges()){ + if (edge.getSrcNodeKey() === node.getKey() || edge.getDestNodeKey() === node.getKey()){ + isConnected = true; + break; + } } } // check if a node is completely disconnected from the graph, which is sometimes an indicator of something wrong - // only check this if the component has been selected in the graph. If it was selected from the palette, it doesnt make sense to complain that it is not connected. + // only check this if the component has been selected in the graph. If it was selected from the palette, it doesn't make sense to complain that it is not connected. if (!isConnected && !(maxInputs === 0 && maxOutputs === 0) && selectedLocation === Eagle.FileType.Graph){ - const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has no connected edges. It should be connected to the graph in some way", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, null, ""); - errorsWarnings.warnings.push(issue); + const issue: ActionMessage = ActionMessage.ShowFix(ActionMessage.Level.Warning, "Node " + node.getKey() + " (" + node.getName() + ") has no connected edges. It should be connected to the graph in some way", function(){Utils.showNode(eagle, selectedLocation, node.getId())}, null, ""); + errors.push(issue); } // check embedded application categories are not 'None' if (node.hasInputApplication() && node.getInputApplication().getCategory() === Category.None){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has input application with category 'None'.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") has input application with category 'None'.")); } if (node.hasOutputApplication() && node.getOutputApplication().getCategory() === Category.None){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has output application with category 'None'.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") has output application with category 'None'.")); } // check that Service nodes have inputApplications with no output ports! if (node.getCategory() === Category.Service && node.hasInputApplication() && node.getInputApplication().getOutputPorts().length > 0){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") is a Service node, but has an input application with at least one output.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Node " + node.getKey() + " (" + node.getName() + ") is a Service node, but has an input application with at least one output.")); } // check the embedded applications if (node.hasInputApplication()){ - Node.isValid(eagle, node.getInputApplication(), selectedLocation, showNotification, showConsole, errorsWarnings); + Node.isValid(eagle, node.getInputApplication(), selectedLocation, showNotification, showConsole, errors); } if (node.hasOutputApplication()){ - Node.isValid(eagle, node.getOutputApplication(), selectedLocation, showNotification, showConsole, errorsWarnings); + Node.isValid(eagle, node.getOutputApplication(), selectedLocation, showNotification, showConsole, errors); } // check that this category of node contains all the fields it requires for (const requirement of Daliuge.categoryFieldsRequired){ if (requirement.categories.includes(node.getCategory())){ for (const requiredField of requirement.fields){ - Node._checkForField(eagle, selectedLocation, node, requiredField, errorsWarnings); + Node._checkForField(eagle, selectedLocation, node, requiredField, errors); } } } @@ -2305,27 +2237,29 @@ export class Node { for (const requirement of Daliuge.categoryTypeFieldsRequired){ if (requirement.categoryTypes.includes(node.getCategoryType())){ for (const requiredField of requirement.fields){ - Node._checkForField(eagle, selectedLocation, node, requiredField, errorsWarnings); + Node._checkForField(eagle, selectedLocation, node, requiredField, errors); } } } - return Utils.worstEdgeError(errorsWarnings); + return ActionList.worstError(errors); } - private static _checkForField = (eagle: Eagle, location: Eagle.FileType, node: Node, field: Field, errorsWarnings: Errors.ErrorsWarnings) : void => { + private static _checkForField = (eagle: Eagle, location: Eagle.FileType, node: Node, field: Field, errorsWarnings: ActionMessage[]) : void => { // check if the node already has this field const existingField = node.getFieldByDisplayText(field.getDisplayText()); // if not, create one by cloning the required field // if so, check the attributes of the field match if (existingField === null){ - const message = "Node " + node.getKey() + " (" + node.getName() + ":" + node.category() + ":" + node.categoryType() + ") does not have the required '" + field.getDisplayText() + "' field"; - errorsWarnings.errors.push(Errors.Show(message, function(){Utils.showNode(eagle, location, node.getId());})); + const message = "Node " + node.getKey() + " (" + node.getName() + ") has category " + node.getCategory() + " but has no '" + field.getDisplayText() + "' field."; + const newField = field.clone(); + newField.setId(Utils.uuidv4()); + errorsWarnings.push(ActionMessage.ShowFix(ActionMessage.Level.Error, message, function(){Utils.showNode(eagle, location, node.getId());}, function(){Utils.fixNodeAddField(node, newField)}, "Add '" + newField.getDisplayText() + "' field to node")); } else { if (existingField.getParameterType() !== field.getParameterType()){ const message = "Node " + node.getKey() + " (" + node.getName() + ") has a '" + field.getDisplayText() + "' field with the wrong parameter type (" + existingField.getParameterType() + "), should be a " + field.getParameterType(); - errorsWarnings.errors.push(Errors.ShowFix(message, function(){Utils.showNode(eagle, location, node.getId());}, function(){Utils.fixFieldParameterType(eagle, node, existingField, field.getParameterType())}, "Switch type of field to '" + field.getParameterType())); + errorsWarnings.push(ActionMessage.ShowFix(ActionMessage.Level.Error, message, function(){Utils.showNode(eagle, location, node.getId());}, function(){Utils.fixFieldParameterType(eagle, node, existingField, field.getParameterType())}, "Switch type of field to '" + field.getParameterType())); } } } diff --git a/src/Palette.ts b/src/Palette.ts index 275e2153b..08d4ab3d4 100644 --- a/src/Palette.ts +++ b/src/Palette.ts @@ -24,14 +24,15 @@ import * as ko from "knockout"; -import {Utils} from './Utils'; -import {Eagle} from './Eagle'; -import {Node} from './Node'; -import {FileInfo} from './FileInfo'; -import {RepositoryFile} from './RepositoryFile'; -import {Errors} from './Errors'; -import {Category} from './Category'; -import {CategoryData} from './CategoryData'; +import { ActionMessage } from "./Action"; +import { Category } from './Category'; +import { CategoryData } from './CategoryData'; +import { Eagle } from './Eagle'; +import { FileInfo } from './FileInfo'; +import { Node } from './Node'; +import { RepositoryFile } from './RepositoryFile'; +import { Utils } from './Utils'; + export class Palette { fileInfo : ko.Observable; @@ -48,27 +49,25 @@ export class Palette { this.searchExclude = ko.observable(false); } - static fromOJSJson = (data : string, file : RepositoryFile, errorsWarnings : Errors.ErrorsWarnings) : Palette => { - // parse the JSON first - const dataObject : any = JSON.parse(data); + static fromOJSJson = (dataObject: any, file : RepositoryFile, errors: ActionMessage[]) : Palette => { const result : Palette = new Palette(); // copy modelData into fileInfo - result.fileInfo(FileInfo.fromOJSJson(dataObject.modelData, errorsWarnings)); + result.fileInfo(FileInfo.fromOJSJson(dataObject.modelData, errors)); // add nodes for (let i = 0 ; i < dataObject.nodeDataArray.length ; i++){ const nodeData = dataObject.nodeDataArray[i]; // read node - const newNode : Node = Node.fromOJSJson(nodeData, errorsWarnings, true, (): number => { + const newNode : Node = Node.fromOJSJson(nodeData, errors, true, (): number => { return Utils.newKey(result.nodes()); }); // check that node has no group if (newNode.getParentKey() !== null){ const error : string = file.name + " Node " + i + " has parentKey: " + newNode.getParentKey() + ". Setting parentKey to null."; - errorsWarnings.warnings.push(Errors.Message(error)); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, error)); newNode.setParentKey(null); } @@ -76,7 +75,7 @@ export class Palette { // check that x, y, position is the default if (newNode.getPosition().x !== 0 || newNode.getPosition().y !== 0){ const error : string = file.name + " Node " + i + " has non-default position: (" + newNode.getPosition().x + "," + newNode.getPosition().y + "). Setting to default."; - errorsWarnings.warnings.push(Errors.Message(error)); + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, error)); newNode.setPosition(0, 0); } @@ -87,16 +86,15 @@ export class Palette { // check for missing name if (result.fileInfo().name === ""){ - const error : string = file.name + " FileInfo.name is empty. Setting name to " + file.name; - errorsWarnings.warnings.push(Errors.Message(error)); + const message : string = file.name + " FileInfo.name is empty. Setting name to " + file.name; + errors.push(ActionMessage.Message(ActionMessage.Level.Warning, message)); result.fileInfo().name = file.name; } // check palette, and then add any resulting errors/warnings to the end of the errors/warnings list const checkResult = Utils.checkPalette(result); - errorsWarnings.errors.push(...checkResult.errors); - errorsWarnings.warnings.push(...checkResult.warnings); + errors.push(...checkResult); return result; } diff --git a/src/QuickActions.ts b/src/QuickActions.ts index b783f1053..21be6ec88 100644 --- a/src/QuickActions.ts +++ b/src/QuickActions.ts @@ -1,9 +1,3 @@ -import {Eagle} from './Eagle'; -import {Category} from './Category'; -import {Utils} from './Utils'; -import {Errors} from './Errors'; -import { Setting } from './Setting'; -import { ParameterTable } from './ParameterTable'; import {KeyboardShortcut} from './KeyboardShortcut'; let wordMatch:any[] = [] diff --git a/src/Repositories.ts b/src/Repositories.ts index 1aa5801b7..4b9bf340f 100644 --- a/src/Repositories.ts +++ b/src/Repositories.ts @@ -34,7 +34,7 @@ export class Repositories { } static selectFile = (file : RepositoryFile) : void => { - // console.log("selectFile() service:", file.repository.service, "repo:", file.repository.name, "branch:", file.repository.branch, "path:", file.path, "file:", file.name, "type:", file.type); + console.log("selectFile() service:", file.repository.service, "repo:", file.repository.name, "branch:", file.repository.branch, "path:", file.path, "file:", file.name, "type:", file.type); const eagle: Eagle = Eagle.getInstance(); // check if the current file has been modified @@ -60,10 +60,10 @@ export class Repositories { return; } - eagle.openRemoteFile(file); + eagle.openRemoteFile(file, null); }); } else { - eagle.openRemoteFile(file); + eagle.openRemoteFile(file, null); } } @@ -107,13 +107,13 @@ export class Repositories { removeCustomRepository = (repository : Repository) : void => { // if settings dictates that we don't confirm with user, remove immediately - if (!Setting.findValue(Setting.CONFIRM_REMOVE_REPOSITORES)){ + if (!Setting.findValue(Setting.CONFIRM_REMOVE_REPOSITORIES)){ this._removeCustomRepository(repository); return; } // otherwise, check with user - Utils.requestUserConfirm("Remove Custom Repository", "Remove this repository from the list?", "OK", "Cancel",Setting.CONFIRM_REMOVE_REPOSITORES, (confirmed : boolean) =>{ + Utils.requestUserConfirm("Remove Custom Repository", "Remove this repository from the list?", "OK", "Cancel",Setting.CONFIRM_REMOVE_REPOSITORIES, (confirmed : boolean) =>{ if (!confirmed){ console.log("User aborted removeCustomRepository()"); return; diff --git a/src/Repository.ts b/src/Repository.ts index 5db779a3b..f88bff315 100644 --- a/src/Repository.ts +++ b/src/Repository.ts @@ -20,6 +20,7 @@ export class Repository { folders : ko.ObservableArray // NOTE: I think we should be able to use the Eagle.RepositoryService.Unknown enum here, but it causes a javascript error. Not sure why. + // TODO: try again! static DUMMY = new Repository("Unknown", "", "", false); constructor(service : Eagle.RepositoryService, name : string, branch : string, isBuiltIn : boolean){ diff --git a/src/RightClick.ts b/src/RightClick.ts index 9dba05a50..eabf87fb8 100644 --- a/src/RightClick.ts +++ b/src/RightClick.ts @@ -1,15 +1,16 @@ -import {Eagle} from './Eagle'; -import {Edge} from './Edge'; -import {Node} from './Node'; +import { Eagle } from './Eagle'; +import { Edge } from './Edge'; +import { Field } from './Field'; +import { GraphRenderer } from './GraphRenderer'; +import { Node } from './Node'; import { Palette } from './Palette'; -import { TutorialSystem } from './Tutorial'; import { Setting } from './Setting'; export class RightClick { static edgeDropSrcNode : Node - static edgeDropSrcPort : any + static edgeDropSrcPort : Field static edgeDropSrcIsInput : boolean constructor(){ @@ -123,6 +124,8 @@ export class RightClick { } }, 300); } + + GraphRenderer.renderDraggingPortEdge(false); } static createHtmlPaletteList = () : string => { @@ -131,10 +134,12 @@ export class RightClick { let paletteList:string = '' const palettes = eagle.palettes() + // add nodes from each palette palettes.forEach(function(palette){ paletteList = paletteList + RightClick.constructHtmlPaletteList(palette.getNodes(),'addNode',null,palette.fileInfo().name) }) + // add nodes from the logical graph paletteList = paletteList + RightClick.constructHtmlPaletteList(eagle.logicalGraph().getNodes(),'addNode',null,'Graph') return paletteList @@ -144,30 +149,13 @@ export class RightClick { const eagle: Eagle = Eagle.getInstance(); let paletteList:string = '' - const palettes = eagle.palettes() - - //toggling showing only filtered nodes or showing all - if(!Setting.findValue(Setting.FILTER_NODE_SUGGESTIONS)){ - let x : Node[] = [] - palettes.forEach(function(palette){ - - palette.getNodes().forEach(function(node){ - x.push(node) - }) - - eagle.logicalGraph().getNodes().forEach(function(graphNode){ - x.push(graphNode) - }) - }) - - compatibleNodesList = x - } + const palettes = eagle.palettes(); palettes.forEach(function(palette){ - paletteList = paletteList+RightClick.constructHtmlPaletteList(palette.getNodes(), 'addAndConnect', compatibleNodesList ,palette.fileInfo().name) + paletteList = paletteList + RightClick.constructHtmlPaletteList(palette.getNodes(), 'addAndConnect', compatibleNodesList, palette.fileInfo().name) }) - paletteList = paletteList + RightClick.constructHtmlPaletteList(eagle.logicalGraph().getNodes(),'addAndConnect',compatibleNodesList,'Graph') + paletteList = paletteList + RightClick.constructHtmlPaletteList(eagle.logicalGraph().getNodes(), 'addAndConnect', compatibleNodesList, 'Graph') return paletteList } @@ -218,13 +206,13 @@ export class RightClick { collectionOfNodes.forEach(function(node){ //this mode is the simplest version for right click adding a node on the graph canvas if(node.isData()){ - dataHtml = dataHtml+``+node.getName()+'' + dataHtml = dataHtml+``+node.getName()+'' dataFound = true }else if (node.isApplication()){ - appHtml = appHtml+``+node.getName()+'' + appHtml = appHtml+``+node.getName()+'' appFound = true }else{ - otherHtml = otherHtml+``+node.getName()+'' + otherHtml = otherHtml+``+node.getName()+'' otherFound = true } nodeFound = true @@ -372,7 +360,7 @@ export class RightClick { } static initiateContextMenu = (data:any, eventTarget:any) : void => { - //graph node specific context menu intitating function, we cannot use ko bindings within the d3 svg + //graph node specific context menu intitating function let passedObjectClass if(data instanceof Node){ @@ -381,7 +369,7 @@ export class RightClick { passedObjectClass = 'rightClick_graphEdge' } - RightClick.requestCustomContextMenu(data,eventTarget, passedObjectClass) + RightClick.requestCustomContextMenu(data, eventTarget, passedObjectClass) // prevent bubbling events event.stopPropagation(); @@ -389,15 +377,14 @@ export class RightClick { static edgeDropCreateNode = (data:any, eventTarget:any) : void => { - RightClick.requestCustomContextMenu(data,eventTarget, 'edgeDropCreate') + RightClick.requestCustomContextMenu(data, eventTarget, 'edgeDropCreate') // prevent bubbling events event.stopPropagation(); } static requestCustomContextMenu = (data:any, targetElement:JQuery, passedObjectClass:string) : void => { - - //getting the mouse event for positioning the right click menu at the cursor location + // getting the mouse event for positioning the right click menu at the cursor location const eagle: Eagle = Eagle.getInstance(); const thisEvent = event as MouseEvent @@ -409,34 +396,24 @@ export class RightClick { Eagle.selectedRightClickObject(data) } + // close any existing context menu + //RightClick.closeCustomContextMenu(true); + $('#customContextMenu').remove(); - let targetClass = '' - - if(passedObjectClass === ''){ - targetClass = $(targetElement).attr('class') - } - - //setting up the menu div - $('#customContextMenu').remove() + // setting up the menu div $(document).find('body').append('
') $('#customContextMenu').css('top',mouseY+'px') $('#customContextMenu').css('left',mouseX+'px') if(passedObjectClass != 'edgeDropCreate'){ - //in change of calculating the right click location as the location where to place the node - const offset = $(targetElement).offset(); - let x = mouseX - offset.left; - let y = mouseY - offset.top; - - // transform display coords into real coords - x = (x - eagle.globalOffsetX)/eagle.globalScale; - y = (y - eagle.globalOffsetY)/eagle.globalScale; - + //here we are grabbing the on graph location of the mouse cursor, this is where we wilkl palce the node when right clicking on the empty graph + let x = GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null) + let y = GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null) Eagle.selectedRightClickPosition = {x:x, y:y}; } - var selectedObjectAmount = eagle.selectedObjects().length - var rightClickObjectInSelection = false + const selectedObjectAmount = eagle.selectedObjects().length + let rightClickObjectInSelection = false if (selectedObjectAmount > 1){ //if more than one node is selected eagle.selectedObjects().forEach(function(selectedObject){ @@ -449,7 +426,7 @@ export class RightClick { if(rightClickObjectInSelection){ // if we right clicked an object that is part of a multi selection - if(passedObjectClass === 'rightClick_graphNode' || passedObjectClass === 'rightClick_graphEdge' || passedObjectClass === 'rightClick_hierarchyNode' || targetClass.includes('rightClick_paletteComponent')){ + if(passedObjectClass === 'rightClick_graphNode' || passedObjectClass === 'rightClick_graphEdge' || passedObjectClass === 'rightClick_hierarchyNode' || passedObjectClass === 'rightClick_paletteComponent'){ $('#customContextMenu').append('Delete') $('#customContextMenu').append('Duplicate') $('#customContextMenu').append('Copy') @@ -457,9 +434,9 @@ export class RightClick { }else{ //if we right clicked an individual object //append function options depending on the right click object - if(targetClass.includes('rightClick_logicalGraph')){ + if(passedObjectClass === 'rightClick_logicalGraph'){ if(Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ - var searchbar = `
+ const searchbar = `
search close @@ -470,7 +447,7 @@ export class RightClick { $('#customContextMenu').append(searchbar) $('#customContextMenu').append('
') - var paletteList = RightClick.createHtmlPaletteList() + const paletteList = RightClick.createHtmlPaletteList() $('#rightClickPaletteList').append(paletteList) @@ -478,12 +455,12 @@ export class RightClick { $('#rightClickSearchBar').focus() RightClick.initiateQuickSelect() }else{ - var message = 'Lacking graph editing permissions' + const message = 'Lacking graph editing permissions' $('#customContextMenu').append(message) } }else if(passedObjectClass === 'edgeDropCreate'){ if(Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ - var searchbar = `
+ const searchbar = `
search close @@ -494,17 +471,17 @@ export class RightClick { $('#customContextMenu').append(searchbar) $('#customContextMenu').append('
') - var paletteList = RightClick.createHtmlEdgeDragList(data) + const paletteList = RightClick.createHtmlEdgeDragList(data) $('#rightClickPaletteList').append(paletteList) Eagle.selectedRightClickLocation(Eagle.FileType.Graph) $('#rightClickSearchBar').focus() RightClick.initiateQuickSelect() }else{ - var message = 'Lacking graph editing permissions' + const message = 'Lacking graph editing permissions' $('#customContextMenu').append(message) } - }else if(targetClass.includes('rightClick_paletteComponent')){ + }else if(passedObjectClass === 'rightClick_paletteComponent'){ Eagle.selectedRightClickLocation(Eagle.FileType.Palette) if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ @@ -513,7 +490,7 @@ export class RightClick { $('#customContextMenu').append('
Delete') $('#customContextMenu').append('Add to another palette') } - }else if(targetClass.includes('rightClick_hierarchyNode')){ + }else if(passedObjectClass === 'rightClick_hierarchyNode'){ Eagle.selectedRightClickLocation(Eagle.FileType.Graph) $('#customContextMenu').append(RightClick.getNodeDescriptionDropdown()) @@ -532,6 +509,7 @@ export class RightClick { $('#customContextMenu').append('Delete') if (data.isConstruct()){ $('#customContextMenu').append('Delete All') + $('#customContextMenu').append('Center Around Children') } if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ $('#customContextMenu').append('Add to palette') @@ -541,7 +519,7 @@ export class RightClick { }else if(passedObjectClass === 'rightClick_graphEdge'){ $('#customContextMenu').append('Delete') - }else if(targetClass.includes('rightClick_paletteHeader')){ + }else if(passedObjectClass === 'rightClick_paletteHeader'){ if(!data.fileInfo().builtIn){ $('#customContextMenu').append('Remove Palette') @@ -566,7 +544,7 @@ export class RightClick { } } // adding a listener to function options that closes the menu if an option is clicked - $('#customContextMenu a').on('click',function(){if($(event.target).parents('.searchBarContainer').length){return};RightClick.closeCustomContextMenu(true)}) + $('#customContextMenu a').on('click',function(){if($(event.target).parents('.searchBarContainer').length){return}RightClick.closeCustomContextMenu(true)}) } } diff --git a/src/Setting.ts b/src/Setting.ts index 6017de045..fadda1467 100644 --- a/src/Setting.ts +++ b/src/Setting.ts @@ -1,8 +1,10 @@ import * as ko from "knockout"; +import { ActionMessage } from "./Action"; import {Eagle} from './Eagle'; import {Utils} from './Utils'; import {UiMode, UiModeSystem, SettingData} from './UiModes'; + export class SettingsGroup { private name : string; private displayFunc : (eagle: Eagle) => boolean; @@ -201,12 +203,29 @@ export class Setting { } resetDefault = () : void => { - let value = this.graphDefaultValue - if(UiModeSystem.getActiveUiMode().getName()==='Minimal'){ - value = this.minimalDefaultValue - }else if(UiModeSystem.getActiveUiMode().getName()==='Expert'){ - value = this.expertDefaultValue + const activeUIModeName: string = UiModeSystem.getActiveUiMode().getName(); + let value: any = this.graphDefaultValue; + + switch (activeUIModeName){ + case "Student": + value = this.studentDefaultValue; + break; + case "Minimal": + value = this.minimalDefaultValue; + break; + case "Graph": + value = this.graphDefaultValue; + break; + case "Component": + value = this.componentDefaultValue; + break; + case "Expert": + value = this.expertDefaultValue; + break; + default: + console.warn("Unknown active UI mode name:", activeUIModeName, ", using default value for ", this.name, " setting"); } + this.value(value); } @@ -227,19 +246,7 @@ export class Setting { } static showInspectorErrorsWarnings = () : boolean => { - const eagle = Eagle.getInstance(); - - switch (Setting.findValue(Setting.SHOW_INSPECTOR_WARNINGS)){ - case Setting.ShowErrorsMode.Warnings: - return eagle.selectedNode().getErrorsWarnings(eagle).errors.length + eagle.selectedNode().getErrorsWarnings(eagle).warnings.length > 0; - break; - case Setting.ShowErrorsMode.Errors: - return eagle.selectedNode().getErrorsWarnings(eagle).errors.length > 0; - break; - case Setting.ShowErrorsMode.None: - default: - return false; - } + return Setting.findValue(Setting.SHOW_INSPECTOR_WARNINGS) != Setting.ShowErrorsMode.None; } static readonly GITHUB_ACCESS_TOKEN_KEY: string = "GitHubAccessToken"; @@ -250,7 +257,7 @@ export class Setting { static readonly ACTION_CONFIRMATIONS : string = "ActionConfirmations"; static readonly CONFIRM_DISCARD_CHANGES : string = "ConfirmDiscardChanges"; static readonly CONFIRM_NODE_CATEGORY_CHANGES : string = "ConfirmNodeCategoryChanges"; - static readonly CONFIRM_REMOVE_REPOSITORES : string = "ConfirmRemoveRepositories"; + static readonly CONFIRM_REMOVE_REPOSITORIES : string = "ConfirmRemoveRepositories"; static readonly CONFIRM_RELOAD_PALETTES : string = "ConfirmReloadPalettes"; static readonly CONFIRM_DELETE_OBJECTS : string = "ConfirmDeleteObjects"; @@ -340,10 +347,9 @@ const settings : SettingsGroup[] = [ new Setting(true, "Reset Action Confirmations", Setting.ACTION_CONFIRMATIONS, "Enable all action confirmation prompts",false, Setting.Type.Button, '', '','','','',[],'$root.resetActionConfirmations()'), new Setting(false, "Confirm Discard Changes", Setting.CONFIRM_DISCARD_CHANGES, "Prompt user to confirm that unsaved changes to the current file should be discarded when opening a new file, or when navigating away from EAGLE.",false, Setting.Type.Boolean, true, true,true,true,true), new Setting(false, "Confirm Node Category Changes", Setting.CONFIRM_NODE_CATEGORY_CHANGES, "Prompt user to confirm that changing the node category may break the node.",false, Setting.Type.Boolean, true, true,true,true,true), - new Setting(false, "Confirm Remove Repositories", Setting.CONFIRM_REMOVE_REPOSITORES, "Prompt user to confirm removing a repository from the list of known repositories.",false , Setting.Type.Boolean, true,true,true,true,true), + new Setting(false, "Confirm Remove Repositories", Setting.CONFIRM_REMOVE_REPOSITORIES, "Prompt user to confirm removing a repository from the list of known repositories.",false , Setting.Type.Boolean, true,true,true,true,true), new Setting(false, "Confirm Reload Palettes", Setting.CONFIRM_RELOAD_PALETTES, "Prompt user to confirm when loading a palette that is already loaded.",false , Setting.Type.Boolean,true,true,true,true,true), new Setting(false, "Confirm Delete", Setting.CONFIRM_DELETE_OBJECTS, "Prompt user to confirm when deleting node(s) or edge(s) from a graph.",false , Setting.Type.Boolean, true,true,true,true,true), - new Setting(false, "Confirm Delete", Setting.CONFIRM_DELETE_OBJECTS, "Prompt user to confirm when deleting node(s) or edge(s) from a graph.",false , Setting.Type.Boolean, true,true,true,true,true), new Setting(true, "Open Default Palette on Startup", Setting.OPEN_DEFAULT_PALETTE, "Open a default palette on startup. The palette contains an example of all known node categories", false, Setting.Type.Boolean, false,false,true,true,true), new Setting(true, "Disable JSON Validation", Setting.DISABLE_JSON_VALIDATION, "Allow EAGLE to load/save/send-to-translator graphs and palettes that would normally fail validation against schema.", false, Setting.Type.Boolean, false,false,false,false,false), new Setting(true, "Overwrite Existing Translator Tab", Setting.OVERWRITE_TRANSLATION_TAB, "When translating a graph, overwrite an existing translator tab", false, Setting.Type.Boolean, true,true,true,true,true), @@ -356,10 +362,10 @@ const settings : SettingsGroup[] = [ new Setting(true, "Show non key parameters", Setting.SHOW_NON_KEY_PARAMETERS, "Show additional parameters that are not marked as key parameters for the current graph",false, Setting.Type.Boolean, false,true,true,true,true), new Setting(true, "Display Node Keys", Setting.DISPLAY_NODE_KEYS, "Display Node Keys", false, Setting.Type.Boolean,false,false,false,true,true), new Setting(false, "Show Developer Tab", Setting.SHOW_DEVELOPER_TAB, "Reveals the developer tab in the settings menu", false, Setting.Type.Boolean, false,false,false,false,true), - new Setting(true, "Translator Mode", Setting.USER_TRANSLATOR_MODE, "Configue the translator mode", false, Setting.Type.Select, Setting.TranslatorMode.Minimal,Setting.TranslatorMode.Minimal,Setting.TranslatorMode.Normal,Setting.TranslatorMode.Normal,Setting.TranslatorMode.Expert, Object.values(Setting.TranslatorMode)), + new Setting(true, "Translator Mode", Setting.USER_TRANSLATOR_MODE, "Configure the translator mode", false, Setting.Type.Select, Setting.TranslatorMode.Minimal,Setting.TranslatorMode.Minimal,Setting.TranslatorMode.Normal,Setting.TranslatorMode.Normal,Setting.TranslatorMode.Expert, Object.values(Setting.TranslatorMode)), new Setting(true, "Graph Zoom Divisor", Setting.GRAPH_ZOOM_DIVISOR, "The number by which zoom inputs are divided before being applied. Larger divisors reduce the amount of zoom.", false, Setting.Type.Number,1000,1000,1000,1000,1000), new Setting(false, "Snap To Grid", Setting.SNAP_TO_GRID, "Align positions of nodes in graph to a grid", false, Setting.Type.Boolean,false,false,false,false,false), - new Setting(true, "Snap To Grid Size", Setting.SNAP_TO_GRID_SIZE, "Size of grid used when aligning positions of nodes in graph (pixels)", false, Setting.Type.Number, 50, 50, 50, 50, 50), + new Setting(false, "Snap To Grid Size", Setting.SNAP_TO_GRID_SIZE, "Size of grid used when aligning positions of nodes in graph (pixels)", false, Setting.Type.Number, 50, 50, 50, 50, 50), new Setting(true, "Show edge/node errors/warnings in inspector", Setting.SHOW_INSPECTOR_WARNINGS, "Show the errors/warnings found for the selected node/edge in the inspector", false, Setting.Type.Select, Setting.ShowErrorsMode.None, Setting.ShowErrorsMode.None, Setting.ShowErrorsMode.Errors, Setting.ShowErrorsMode.Errors,Setting.ShowErrorsMode.Errors, Object.values(Setting.ShowErrorsMode)), new Setting(false, "Right Window Width", Setting.RIGHT_WINDOW_WIDTH_KEY, "saving the width of the right window", true, Setting.Type.Number,400,400,400,400,400), new Setting(false, "Left Window Width", Setting.LEFT_WINDOW_WIDTH_KEY, "saving the width of the left window", true, Setting.Type.Number, 310, 310, 310, 310, 310), @@ -369,14 +375,14 @@ const settings : SettingsGroup[] = [ "Advanced Editing", () => {return true;}, [ - new Setting(true,"Allow Invalid edges", Setting.ALLOW_INVALID_EDGES, "Allow the user to create edges even if they would normally be determined invalid.", false, Setting.Type.Boolean, false, false, false, false, true), + new Setting(true, "Allow Invalid edges", Setting.ALLOW_INVALID_EDGES, "Allow the user to create edges even if they would normally be determined invalid.", false, Setting.Type.Boolean, false, false, false, false, true), new Setting(true, "Allow Component Editing", Setting.ALLOW_COMPONENT_EDITING, "Allow the user to add/remove ports and parameters from components.",false, Setting.Type.Boolean,false, false, false, true,true), new Setting(true, "Allow Set Key Parameter", Setting.ALLOW_SET_KEY_PARAMETER, "Allow the user to add/remove key parameter flags from parameters.", false, Setting.Type.Boolean,false, true, true, true,true), new Setting(true, "Allow Graph Editing", Setting.ALLOW_GRAPH_EDITING, "Allow the user to edit and create graphs.", false, Setting.Type.Boolean, false, false, true, true, true), new Setting(true, "Allow Palette Editing", Setting.ALLOW_PALETTE_EDITING, "Allow the user to edit palettes.", false, Setting.Type.Boolean, false, false, false, true, true), new Setting(true, "Allow Readonly Palette Editing", Setting.ALLOW_READONLY_PALETTE_EDITING, "Allow the user to modify palettes that would otherwise be readonly.", false, Setting.Type.Boolean,false,false,false,false,true), new Setting(true, "Allow Edge Editing", Setting.ALLOW_EDGE_EDITING, "Allow the user to edit edge attributes.", false, Setting.Type.Boolean, false, false,false, false, true), - new Setting(true, "Filter Node Suggestions", Setting.FILTER_NODE_SUGGESTIONS, "Filter Node Options When Drawing Edges Into Empty Space", false, Setting.Type.Boolean,true,true,true,true,true), + new Setting(true, "Filter Node Suggestions", Setting.FILTER_NODE_SUGGESTIONS, "Filter Node Options When Drawing Edges Into Empty Space", false, Setting.Type.Boolean,true,true,true,true,false), new Setting(false, "STUDENT_SETTINGS_MODE", Setting.STUDENT_SETTINGS_MODE, "Mode disabling setting editing for students.", false, Setting.Type.Boolean, true, false,false, false, false), new Setting(true, "Value Editing", Setting.VALUE_EDITING_PERMS, "Set which values are allowed to be edited.", false, Setting.Type.Select, Setting.valueEditingPerms.KeyOnly,Setting.valueEditingPerms.Normal,Setting.valueEditingPerms.Normal,Setting.valueEditingPerms.ReadOnly,Setting.valueEditingPerms.ReadOnly, Object.values(Setting.valueEditingPerms)), ] diff --git a/src/Translator.ts b/src/Translator.ts index 16a1e6bef..e2a216540 100644 --- a/src/Translator.ts +++ b/src/Translator.ts @@ -167,16 +167,7 @@ export class Translator { } // validate json - if (!Setting.findValue(Setting.DISABLE_JSON_VALIDATION)){ - const jsonObject = JSON.parse(jsonString); - const validatorResult : {valid: boolean, errors: string} = Utils.validateJSON(jsonObject, format, Eagle.FileType.Graph); - if (!validatorResult.valid){ - const message = "JSON Output failed validation against internal JSON schema, saving anyway"; - console.error(message, validatorResult.errors); - Utils.showUserMessage("Error", message + "
" + validatorResult.errors); - //return; - } - } + Utils.validateJSON(jsonString, Eagle.FileType.Graph); const translatorData = { algo: algorithmName, diff --git a/src/Tutorial.ts b/src/Tutorial.ts index da506c8a0..2bb41e86f 100644 --- a/src/Tutorial.ts +++ b/src/Tutorial.ts @@ -1,4 +1,3 @@ -import { active } from 'd3'; import {Eagle} from './Eagle'; @@ -94,7 +93,13 @@ export class TutorialSystem { static initiateFindGraphNodeIdByNodeName = (name:string) : JQuery => { const eagle = Eagle.getInstance() - let x = $('#'+eagle.logicalGraph().findNodeGraphIdByNodeName(name)+' rect') + let x = $('#logicalGraph #'+eagle.logicalGraph().findNodeGraphIdByNodeName(name)+'.container') + return x + } + + static initiateSimpleFindGraphNodeIdByNodeName = (name:string) : string => { + const eagle = Eagle.getInstance() + let x = eagle.logicalGraph().findNodeGraphIdByNodeName(name) return x } @@ -335,6 +340,7 @@ export class Tutorial { } highlightStepTarget = (target: JQuery): void => { + const eagle = Eagle.getInstance() if(TutorialSystem.activeTutCurrentStep.getAlternateHightlightTargetFunc() != null){ target = TutorialSystem.activeTutCurrentStep.getAlternateHightlightTargetFunc()() } @@ -343,10 +349,16 @@ export class Tutorial { const coords = target.offset() const docWidth = window.innerWidth const top_actual = Math.round(coords.top)//distance of the top of the element from the top of the document - const right = coords.left + $(target).outerWidth() + let right = coords.left + $(target).outerWidth() const left = docWidth - coords.left - const targetHeight = Math.round($(target).outerHeight()) - const bottom_actual = Math.round(coords.top + targetHeight) //distance from the bottom of the target element to the bottom of the document + let targetHeight = Math.round($(target).outerHeight()) + let bottom_actual = Math.round(coords.top + targetHeight) //distance from the bottom of the target element to the bottom of the document + + if(target.parents('#logicalGraphParent').length){ + targetHeight = Math.round(targetHeight*eagle.globalScale()) + right = coords.left+$(target).outerWidth() *eagle.globalScale() + bottom_actual = Math.round(coords.top + targetHeight) + } //i am appending these once if they dont exist. they are then adjusted for each step. and finally removed when exiting the tutorial if ($('.tutorialHighlight').length === 0) { diff --git a/src/Undo.ts b/src/Undo.ts index 8e2b80ef6..ab152dee9 100644 --- a/src/Undo.ts +++ b/src/Undo.ts @@ -24,6 +24,7 @@ import * as ko from "knockout"; +import { ActionMessage } from "./Action"; import {Config} from './Config'; import {Eagle} from './Eagle'; import {LogicalGraph} from './LogicalGraph'; @@ -122,7 +123,7 @@ export class Undo { Undo.printTable(); } - eagle.checkGraph(); + eagle.graphChecker().check(); } nextSnapshot = (eagle: Eagle) : void => { @@ -139,7 +140,7 @@ export class Undo { Undo.printTable(); } - eagle.checkGraph(); + eagle.graphChecker().check(); } toString = () : string => { @@ -173,7 +174,6 @@ export class Undo { } const dataObject: LogicalGraph = snapshot.data(); - eagle.logicalGraph(dataObject); } diff --git a/src/Utils.ts b/src/Utils.ts index 5f3ee4017..2f0702b17 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -26,13 +26,14 @@ import * as Ajv from "ajv"; import * as Showdown from "showdown"; import * as ko from "knockout"; +import { ActionList } from "./ActionList"; +import { ActionMessage } from "./Action"; import {Category} from './Category'; import {CategoryData} from "./CategoryData"; -import {Config} from './Config'; +import { ComponentUpdater } from "./ComponentUpdater"; import {Daliuge} from './Daliuge'; import {Eagle} from './Eagle'; import {Edge} from './Edge'; -import {Errors} from './Errors'; import {Field} from './Field'; import {KeyboardShortcut} from './KeyboardShortcut'; import {LogicalGraph} from './LogicalGraph'; @@ -42,6 +43,7 @@ import {PaletteInfo} from './PaletteInfo'; import {Repository} from './Repository'; import {Setting} from './Setting'; import {FileInfo} from "./FileInfo"; +import { RepositoryFile } from "./RepositoryFile"; import { UiModeSystem } from "./UiModes"; export class Utils { @@ -93,6 +95,10 @@ export class Utils { return now.getFullYear() + "-" + Utils.padStart(now.getMonth() + 1, 2) + "-" + Utils.padStart(now.getDate(), 2) + "-" + Utils.padStart(now.getHours(), 2) + "-" + Utils.padStart(now.getMinutes(), 2) + "-" + Utils.padStart(now.getSeconds(), 2); } + static generateGraphName(): string { + return "Diagram-" + Utils.generateDateTimeString() + "." + Utils.getDiagramExtension(Eagle.FileType.Graph); + } + static findNewKey(usedKeys : number[]): number { for (let i = -1 ; ; i--){ let found = false; @@ -142,9 +148,9 @@ export class Utils { return usedKeys; } - static newKey(nodes: Node[]): number { - const usedKeys = Utils.getUsedKeys(nodes); - return Utils.findNewKey(usedKeys); + static newKey(nodes: Node[],usedKeys:number[] = []): number { + const allUsedKeys = Utils.getUsedKeys(nodes).concat(usedKeys); + return Utils.findNewKey(allUsedKeys); } static setEmbeddedApplicationNodeKeys(lg: LogicalGraph): void { @@ -438,24 +444,29 @@ export class Utils { } } - static showErrorsModal(title: string){ - const errors: Errors.Issue[] = Errors.getErrors(); - const warnings: Errors.Issue[] = Errors.getWarnings(); + static showActionListModal(title: string, mode: ActionList.Mode, combinedMessages: {source: string, messages: ActionMessage[]}[]){ + const flatMessages: ActionMessage[] = []; + for (const load of combinedMessages){ + for (const am of load.messages){ + flatMessages.push(new ActionMessage(am.level, load.source + ": " + am.message, am.show, am.fix, am.fixDescription)); + } + } - console.log("showErrorsModal() errors:", errors.length, "warnings:", warnings.length); + const eagle: Eagle = Eagle.getInstance(); + eagle.actionList().mode(mode); + eagle.actionList().messages(flatMessages); - $('#errorsModalTitle').text(title); + $('#actionListModalTitle').text(title); // hide whole errors or warnings sections if none are found - $('#errorsModalErrorsAccordionItem').toggle(errors.length > 0); - $('#errorsModalWarningsAccordionItem').toggle(warnings.length > 0); + $('#actionListModalMessagesAccordionItem').toggle(flatMessages.length > 0); - $('#errorsModal').modal("toggle"); + $('#actionListModal').modal("toggle"); } static showNotification(title : string, message : string, type : "success" | "info" | "warning" | "danger") : void { $.notify({ - title:title + ":", + title:title + ":
", message:message }, { type: type, @@ -591,7 +602,6 @@ export class Utils { }else{ $('#confirmModalDontShowAgain button').text('check_box_outline_blank') } - console.log($('#confirmModalDontShowAgain button').text()) }) } @@ -732,8 +742,8 @@ export class Utils { $('#shortcutsModal').modal("hide"); } - static closeErrorsModal() : void { - $('#errorsModal').modal("hide"); + static closeCheckGraphModal() : void { + $('#checkGraphModal').modal("hide"); } static showPalettesModal(eagle: Eagle) : void { @@ -1395,9 +1405,9 @@ export class Utils { return type0 === type1; } - static checkPalette(palette: Palette): Errors.ErrorsWarnings { + static checkPalette(palette: Palette): ActionMessage[] { const eagle: Eagle = Eagle.getInstance(); - const errorsWarnings: Errors.ErrorsWarnings = {warnings: [], errors: []}; + const errors: ActionMessage[] = []; // check for duplicate keys const keys: number[] = []; @@ -1405,7 +1415,7 @@ export class Utils { for (const node of palette.getNodes()){ // check existing keys if (keys.indexOf(node.getKey()) !== -1){ - errorsWarnings.errors.push(Errors.Message("Key " + node.getKey() + " used by multiple components in palette.")); + errors.push(ActionMessage.Message(ActionMessage.Level.Error, "Key " + node.getKey() + " used by multiple components in palette.")); } else { keys.push(node.getKey()); } @@ -1413,77 +1423,47 @@ export class Utils { // check all nodes are valid for (const node of palette.getNodes()){ - Node.isValid(eagle, node, Eagle.FileType.Palette, false, false, errorsWarnings); + Node.isValid(eagle, node, Eagle.FileType.Palette, false, false, errors); } - return errorsWarnings; + return errors; } - static checkGraph(eagle: Eagle): Errors.ErrorsWarnings { - const errorsWarnings: Errors.ErrorsWarnings = {warnings: [], errors: []}; + static checkGraph(eagle: Eagle): ActionMessage[] { + const errors: ActionMessage[] = []; const graph: LogicalGraph = eagle.logicalGraph(); // check all nodes are valid for (const node of graph.getNodes()){ - Node.isValid(eagle, node, Eagle.FileType.Graph, false, false, errorsWarnings); + Node.isValid(eagle, node, Eagle.FileType.Graph, false, false, errors); } // check all edges are valid for (const edge of graph.getEdges()){ - Edge.isValid(eagle, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, false, errorsWarnings); - } - - // check that all node, edge, field ids are unique - { - const ids : string[] = []; - - // loop over graph nodes - for (const node of graph.getNodes()){ - if (ids.includes(node.getId())){ - const issue: Errors.Issue = Errors.ShowFix( - "Node (" + node.getName() + ") does not have a unique id", - function(){Utils.showNode(eagle, Eagle.FileType.Graph, node.getId())}, - function(){Utils.newId(node)}, - "Assign node a new id" - ); - errorsWarnings.errors.push(issue); - } - ids.push(node.getId()); - - for (const field of node.getFields()){ - if (ids.includes(field.getId())){ - const issue: Errors.Issue = Errors.ShowFix( - "Field (" + field.getDisplayText() + ") on node (" + node.getName() + ") does not have a unique id", - function(){Utils.showNode(eagle, Eagle.FileType.Graph, node.getId())}, - function(){Utils.newId(field)}, - "Assign field a new id" - ); - errorsWarnings.errors.push(issue); - } - ids.push(field.getId()); - } - } + Edge.isValid(graph, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, false, errors); + } - // loop over graph edges - for (const edge of graph.getEdges()){ - if (ids.includes(edge.getId())){ - const issue: Errors.Issue = Errors.ShowFix( - "Edge (" + edge.getId() + ") does not have a unique id", - function(){Utils.showEdge(eagle, edge.getId())}, - function(){Utils.newId(edge)}, - "Assign edge a new id" - ); - errorsWarnings.errors.push(issue); - } - ids.push(edge.getId()); - } + return errors; + } + + // validate json + static validateJSON(jsonString: string, fileType: Eagle.FileType){ + // if validation disabled, just return true + if (Setting.findValue(Setting.DISABLE_JSON_VALIDATION)){ + return; } - return errorsWarnings; + const jsonObject = JSON.parse(jsonString); + const validatorResult : {valid: boolean, errors: string} = Utils._validateJSON(jsonObject, Daliuge.SchemaVersion.OJS, fileType); + if (!validatorResult.valid){ + const message = "JSON Output failed validation against internal JSON schema, saving anyway"; + console.error(message, validatorResult.errors); + Utils.showUserMessage("Error", message + "
" + validatorResult.errors); + } } - static validateJSON(json : object, version : Daliuge.SchemaVersion, fileType : Eagle.FileType) : {valid: boolean, errors: string} { + static _validateJSON(json : object, version : Daliuge.SchemaVersion, fileType : Eagle.FileType) : {valid: boolean, errors: string} { // console.log("validateJSON(): version:", version, " fileType:", fileType); const ajv = new Ajv(); @@ -1562,32 +1542,13 @@ export class Utils { link.click(); } - // https://noonat.github.io/intersect/#aabb-vs-aabb - static nodesOverlap(n0x: number, n0y: number, n0width: number, n0height: number, n1x: number, n1y: number, n1width: number, n1height: number) : boolean { - const n0pos = {x:n0x + n0width/2, y:n0y + n0height/2}; - const n1pos = {x:n1x + n1width/2, y:n1y + n1height/2}; - const n0half = {x:n0width/2, y:n0height/2}; - const n1half = {x:n1width/2, y:n1height/2}; - - //console.log("compare", n0x, n0y, n0width, n0height, n1x, n1y, n1width, n1height); - - const dx = n0pos.x - n1pos.x; - const px = (n0half.x + n1half.x) - Math.abs(dx); - if (px <= 0) { - //console.log("compare OK"); - return false; - } - - const dy = n0pos.y - n1pos.y; - const py = (n0half.y + n1half.y) - Math.abs(dy); - if (py <= 0) { - //console.log("compare OK"); - return false; - } - //console.log("compares HIT"); + static nodesOverlap(n0x: number, n0y: number, n0radius: number, n1x: number, n1y: number, n1radius: number) : boolean { + const dx = n0x - n1x; + const dy = n0y - n1y; + const distance = Math.sqrt(dx*dx + dy*dy); - return true; + return distance <= (n0radius + n1radius); } static table2CSV(table: any[]) : string { @@ -1607,16 +1568,15 @@ export class Utils { return s; } - // https://stackoverflow.com/questions/5254838/calculating-distance-between-a-point-and-a-rectangular-box-nearest-point static positionToNodeDistance(positionX: number, positionY: number, node: Node): number { - const rectMinX = node.getPosition().x; - const rectMaxX = node.getPosition().x + node.getWidth(); - const rectMinY = node.getPosition().y; - const rectMaxY = node.getPosition().y + node.getHeight(); + // first determine the distance between the position and node center + const dx = node.getPosition().x - positionX; + const dy = node.getPosition().y - positionY; + let distance = Math.sqrt(dx*dx + dy*dy); - const dx = Math.max(rectMinX - positionX, 0, positionX - rectMaxX); - const dy = Math.max(rectMinY - positionY, 0, positionY - rectMaxY); - return Math.sqrt(dx*dx + dy*dy); + // then subtract the radius, limit to zero + distance = Math.max(distance - node.getRadius(), 0); + return distance; } @@ -1651,7 +1611,7 @@ export class Utils { static getShortcutDisplay = () : {description:string, shortcut : string,function:string}[] => { const displayShortcuts : {description:string, shortcut : string, function : any} []=[]; - const eagle = (window).eagle; + const eagle: Eagle = Eagle.getInstance(); for (const object of Eagle.shortcuts){ // skip if shortcut should not be displayed @@ -1726,9 +1686,7 @@ export class Utils { return value.toLowerCase() === "true"; } - static fixEdgeType(eagle: Eagle, edgeId: string, newType: string) : void { - const edge = eagle.logicalGraph().findEdgeById(edgeId); - + static fixEdgeType(edge: Edge, newType: string) : void { if (edge === null){ return; } @@ -1736,19 +1694,20 @@ export class Utils { edge.setDataType(newType); } - static fixDeleteEdge(eagle: Eagle, edgeId: string): void { - eagle.logicalGraph().removeEdgeById(edgeId); + static fixDeleteEdge(logicalGraph: LogicalGraph, edgeId: string): void { + logicalGraph.removeEdgeById(edgeId); } - static fixPortType(eagle: Eagle, sourcePort: Field, destinationPort: Field): void { + static fixPortType(sourcePort: Field, destinationPort: Field): void { destinationPort.setType(sourcePort.getType()); } - static fixNodeAddField(eagle: Eagle, node: Node, field: Field){ + static fixNodeAddField(node: Node, field: Field){ node.addField(field); } - static fixNodeFieldIds(eagle: Eagle, nodeKey: number){ + static fixNodeFieldIds(nodeKey: number){ + const eagle: Eagle = Eagle.getInstance(); const node: Node = eagle.logicalGraph().findNodeByKey(nodeKey); if (node === null){ @@ -1762,12 +1721,13 @@ export class Utils { } } - static fixNodeCategory(eagle: Eagle, node: Node, category: Category){ + static fixNodeCategory(eagle: Eagle, node: Node, category: Category, categoryType: Category.Type){ node.setCategory(category); + node.setCategoryType(categoryType); } // NOTE: merges field1 into field0 - static fixNodeMergeFieldsByIndex(eagle: Eagle, node: Node, field0Index: number, field1Index: number){ + static fixNodeMergeFieldsByIndex(eagle: Eagle, location: Eagle.FileType, node: Node, field0Index: number, field1Index: number){ //console.log("fixNodeMergeFieldsByIndex()", node.getName(), field0Index, field1Index); // abort if one or more of the fields is not found @@ -1790,11 +1750,13 @@ export class Utils { field0.setUsage(newUsage); // update all edges to use new field - this._mergeEdges(eagle, field1.getId(), field0.getId()); + if (location === Eagle.FileType.Graph){ + this._mergeEdges(eagle.logicalGraph(), field1.getId(), field0.getId()); + } } // NOTE: merges field1 into field0 - static fixNodeMergeFields(eagle: Eagle, node: Node, field0: Field, field1: Field){ + static fixNodeMergeFields(eagle: Eagle, location: Eagle.FileType, node: Node, field0: Field, field1: Field){ //console.log("fixNodeMergeFieldsById()", node.getName(), field0.getDisplayText(), field1.getDisplayText()); // abort if one or more of the fields is not found @@ -1817,7 +1779,9 @@ export class Utils { field0.setUsage(newUsage); // update all edges to use new field - this._mergeEdges(eagle, field1.getId(), field0.getId()); + if (location === Eagle.FileType.Graph){ + this._mergeEdges(eagle.logicalGraph(), field1.getId(), field0.getId()); + } } static _mergeUsage(usage0: Daliuge.FieldUsage, usage1: Daliuge.FieldUsage) : Daliuge.FieldUsage { @@ -1839,9 +1803,9 @@ export class Utils { return result; } - static _mergeEdges(eagle: Eagle, oldFieldId: string, newFieldId: string){ + static _mergeEdges(logicalGraph: LogicalGraph, oldFieldId: string, newFieldId: string){ // update all edges to use new field - for (const edge of eagle.logicalGraph().getEdges()){ + for (const edge of logicalGraph.getEdges()){ // update src port if (edge.getSrcPortId() === oldFieldId){ edge.setSrcPortId(newFieldId); @@ -1854,11 +1818,11 @@ export class Utils { } } - static fixFieldId(eagle: Eagle, field: Field){ + static fixFieldId(field: Field){ field.setId(Utils.uuidv4()); } - static fixFieldValue(eagle: Eagle, node: Node, exampleField: Field, value: string){ + static fixFieldValue(node: Node, exampleField: Field, value: string){ let field : Field = node.getFieldByDisplayText(exampleField.getDisplayText()); // if a field was not found, clone one from the example and add to node @@ -1871,7 +1835,7 @@ export class Utils { field.setValue(value); } - static fixFieldDefaultValue(eagle: Eagle, field: Field){ + static fixFieldDefaultValue(field: Field){ // depends on the type switch(field.getType()){ case Daliuge.DataType.Boolean: @@ -1892,7 +1856,7 @@ export class Utils { } } - static fixFieldType(eagle: Eagle, field: Field){ + static fixFieldType(field: Field){ if (field.getType() === Daliuge.DataType.Unknown){ field.setType(Daliuge.DataType.Object); return; @@ -1944,16 +1908,11 @@ export class Utils { field.setParameterType(newType); } - static callFixFunc(eagle: Eagle, fixFunc: () => void){ - fixFunc(); - Utils.postFixFunc(eagle); - } - static postFixFunc(eagle: Eagle){ eagle.selectedObjects.valueHasMutated(); eagle.logicalGraph().fileInfo().modified = true; - eagle.checkGraph(); + eagle.graphChecker().check(); eagle.undo().pushSnapshot(eagle, "Fix"); } @@ -1961,9 +1920,14 @@ export class Utils { object.setId(Utils.uuidv4()); } - static showEdge(eagle: Eagle, edgeId: string): void { + static showEdge(edgeId: string): void { + const eagle: Eagle = Eagle.getInstance(); + // close errors modal if visible - $('#errorsModal').modal("hide"); + $('#checkGraphModal').modal("hide"); + + // close action list modal if visible + $('#actionListModal').modal("hide"); eagle.setSelection(Eagle.RightWindowMode.Inspector, eagle.logicalGraph().findEdgeById(edgeId), Eagle.FileType.Graph); } @@ -1972,7 +1936,10 @@ export class Utils { console.log("showNode()", location, nodeId); // close errors modal if visible - $('#errorsModal').modal("hide"); + $('#checkGraphModal').modal("hide"); + + // close action list modal if visible + $('#actionListModal').modal("hide"); // find node from nodeKey let n: Node = null; @@ -2000,18 +1967,19 @@ export class Utils { } // only update result if it is worse that current result - static worstEdgeError(errorsWarnings: Errors.ErrorsWarnings) : Eagle.LinkValid { - if (errorsWarnings === null){ - console.warn("errorsWarnings is null"); + static worstEdgeError(errors: ActionMessage[]) : Eagle.LinkValid { + if (errors === null){ + console.warn("errors is null"); return Eagle.LinkValid.Valid; } - if (errorsWarnings.warnings.length === 0 && errorsWarnings.errors.length === 0){ + if (errors.length === 0){ return Eagle.LinkValid.Valid; } - if (errorsWarnings.errors.length !== 0){ - return Eagle.LinkValid.Invalid; + if (errors.length !== 0){ + // TODO: loop through the errors and find the worst + return Eagle.LinkValid.Impossible; } return Eagle.LinkValid.Warning; @@ -2046,12 +2014,12 @@ export class Utils { "category":node.getCategory(), "categoryType":node.getCategoryType(), "expanded":node.getExpanded(), + "peek":node.isPeek(), "x":node.getPosition().x, "y":node.getPosition().y, - "realX":node.getRealPosition().x, - "realY":node.getRealPosition().y, - "width":node.getWidth(), - "height":node.getHeight(), + // "realX":node.getRealPosition().x, + // "realY":node.getRealPosition().y, + "radius":node.getRadius(), "inputAppKey":node.getInputApplication() === null ? null : node.getInputApplication().getKey(), "inputAppCategory":node.getInputApplication() === null ? null : node.getInputApplication().getCategory(), "inputAppEmbedKey":node.getInputApplication() === null ? null : node.getInputApplication().getEmbedKey(), @@ -2219,7 +2187,8 @@ export class Utils { return result; } - static openRemoteFileFromUrl(repositoryService : Eagle.RepositoryService, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string, callback: (error : string, data : string) => void ) : void { - Utils.httpGet(fileName, callback); + static openRemoteFileFromUrl(file: RepositoryFile, callback: (error : string, data : string) => void ) : void { + Utils.httpGet(file.path, callback); } + } diff --git a/src/bindingHandlers/eagleRightClick.ts b/src/bindingHandlers/eagleRightClick.ts index 26cf541c1..490aa7583 100644 --- a/src/bindingHandlers/eagleRightClick.ts +++ b/src/bindingHandlers/eagleRightClick.ts @@ -11,9 +11,10 @@ ko.bindingHandlers.eagleRightClick = { jQueryElement.on('contextmenu', function(e){ e.preventDefault(); e.stopPropagation(); - const data = ko.unwrap(valueAccessor()); + const data = ko.unwrap(valueAccessor()).data; + const type = ko.unwrap(valueAccessor()).type; - RightClick.requestCustomContextMenu(data,jQueryElement,'') + RightClick.requestCustomContextMenu(data,jQueryElement,type) }) }, update: function (element, valueAccessor) { diff --git a/src/bindingHandlers/graphRenderer.ts b/src/bindingHandlers/graphRenderer.ts deleted file mode 100644 index 4b6666ea7..000000000 --- a/src/bindingHandlers/graphRenderer.ts +++ /dev/null @@ -1,3906 +0,0 @@ -/* eslint-enable @typescript-eslint/no-unused-vars */ - -import * as ko from "knockout"; -import * as d3 from "d3"; -import * as $ from "jquery"; - -import { Category} from '../Category'; -import { CategoryData} from '../CategoryData'; -import { Config} from '../Config'; -import { Daliuge } from "../Daliuge"; -import { Eagle} from '../Eagle'; -import { Edge} from '../Edge'; -import { Field} from '../Field'; -import { LogicalGraph} from '../LogicalGraph'; -import { Node} from '../Node'; -import { RightClick } from "../RightClick"; -import { Setting} from '../Setting'; -import { Utils} from '../Utils'; - - - -ko.bindingHandlers.graphRenderer = { - init: function(element, valueAccessor, allBindings, viewModel, bindingContext : ko.BindingContext) { - //console.log("bindingHandlers.graphRenderer.init()"); - }, - update: function(element, valueAccessor, allBindings, viewModel, bindingContext : ko.BindingContext) { - //console.log("bindingHandlers.graphRenderer.update()"); - - const graph : LogicalGraph = ko.unwrap(valueAccessor()); - - if (graph === null){ - //console.warn("graphRenderer update(): graph is null"); - return; - } - - $(element).empty(); - - render(graph, element.id, bindingContext.$root); - } -}; - -enum LINK_COLORS { - DEFAULT = 'dimgrey', - DEFAULT_SELECTED = 'rgb(47 22 213)', - WARNING = 'orange', - WARNING_SELECTED = 'rgb(47 22 213)', - INVALID = 'red', - INVALID_SELECTED = 'rgb(47 22 213)', - VALID = 'limegreen', - EVENT = 'rgb(128,128,255)', - EVENT_SELECTED = 'rgb(47 22 213)', - AUTO_COMPLETE = 'purple', - CLOSES_LOOP = 'dimgrey', - CLOSES_LOOP_SELECTED = 'rgb(47 22 213)' -} - -function render(graph: LogicalGraph, elementId : string, eagle : Eagle){ - const startTime: number = performance.now(); - eagle.rendererFrameCountRender = eagle.rendererFrameCountRender + 1; - - // sort the nodes array so that groups appear first, this ensures that child nodes are drawn on top of the group their parents - const nodeData : Node[] = depthFirstTraversalOfNodes(graph, eagle.showDataNodes()); - const linkData : Edge[] = getEdges(graph, eagle.showDataNodes()); - - let hasDraggedBackground : boolean = false; - let isDraggingNode : boolean = false; - let draggingInGraph : boolean = false; - let isDraggingSelectionRegion : boolean = false; - let sourcePort: Field | null = null; - let sourceNode: Node | null = null; - let sourcePortIsInput : boolean; - let destinationPort: Field | null = null; - let destinationNode: Node | null = null; - let suggestedPort: Field | null = null; - let suggestedNode: Node | null = null; - let isDraggingPort : boolean = false; - let isDraggingPortValid : Eagle.LinkValid = Eagle.LinkValid.Unknown; - let isDraggingWithAlt : boolean = false; - let dragEventCount : number = 0; - let draggingNodeId : string = ""; - - const mousePosition = {x:0, y:0}; - const selectionRegionStart = {x:0, y:0}; - const selectionRegionEnd = {x:0, y:0}; - const dragStart = {x:0, y:0}; - const headerHeight = 57.78 + 30 - - const DOUBLE_CLICK_DURATION : number = 200; - - const APPS_HEIGHT : number = 28; - const PORT_HEIGHT : number = 24; - - const NODE_STROKE_WIDTH : number = 3; - const HEADER_INSET : number = NODE_STROKE_WIDTH - 4; - - const PORT_OFFSET_X : number = 2; - const PORT_ICON_HEIGHT : number = 12; - const PORT_INSET : number = 10; - - const RESIZE_CONTROL_SIZE : number = 16; - const SHRINK_BUTTON_SIZE : number = 16; - - const RESIZE_BUTTON_LABEL : string = "\u25F2"; - const SHRINK_BUTTON_LABEL : string = "\u21B9"; - - const HEADER_TEXT_FONT_SIZE : number = 14; - const CONTENT_TEXT_FONT_SIZE : number = 14; - const PORT_LABEL_FONT_SIZE : number = 14; - const RESIZE_BUTTON_LABEL_FONT_SIZE : number = 24; - const HEADER_BUTTON_LABEL_FONT_SIZE : number = 12; - - const SHRINK_BUTTONS_ENABLED : boolean = true; - - const MIN_AUTO_COMPLETE_EDGE_RANGE : number = 150; - - const snapToGridSize : number = Setting.findValue(Setting.SNAP_TO_GRID_SIZE); - - const svgContainer = d3 - .select("#" + elementId) - .append("svg"); - - // add a grid pattern to the svg - const svgDefs = svgContainer.append("defs"); - const svgDefsPattern = svgDefs - .append("pattern") - .attr("id", "grid") - .attr("width", snapToGridSize) - .attr("height", snapToGridSize) - .attr("patternUnits", "userSpaceOnUse"); - svgDefsPattern - .append("path") - .attr("d", "M " + snapToGridSize + " 0 L 0 0 0 " + snapToGridSize) - .attr("fill", "none") - .attr("stroke", "grey") - .attr("stroke-width", 0.5); - - // add a root node to the SVG, we'll scale this root node - const rootContainer = svgContainer - .append("g") - .attr("class", "root") - .attr("id", "root") - .attr("transform", rootScaleTranslation); - - // add def for markers - const defs = rootContainer.append("defs"); - - //generating defs from colors array - Object.entries(LINK_COLORS).forEach(function (value, i) { - const newArrowhead = defs - .append("marker") - .attr("id", value[0]) - .attr("viewBox", "0 0 10 10") - .attr("refX", "7") - .attr("refY", "5") - .attr("markerUnits", "strokeWidth") - .attr("markerWidth","8") - .attr("markerHeight", "6") - .attr("orient", "auto"); - - newArrowhead - .append("path") - .attr("d", "M 0 0 L 10 5 L 0 10 z") - .attr("stroke", "none") - .attr("fill", value[1]); - }) - - // background (and grid if enabled) - const rectWidth = $('#logicalGraphD3Div').width(); - const rectHeight = $('#logicalGraphD3Div').height(); - const offsetX = -eagle.globalOffsetX / eagle.globalScale; - const offsetY = -eagle.globalOffsetY / eagle.globalScale; - - rootContainer - .append("rect") - .attr("class", "background") - .attr("fill", eagle.snapToGrid() ? "url(#grid)" : "transparent") - .attr("x", offsetX) - .attr("y", offsetY) - .attr("width", rectWidth*10) - .attr("height", rectHeight*10); - - $("#logicalGraphD3Div svg").mousedown(function(e:any){ - if(e.button === 2){ - return - } - e.preventDefault() - hasDraggedBackground = false; - draggingInGraph = true; - - if (e.shiftKey || e.altKey){ - hasDraggedBackground = true; - isDraggingSelectionRegion = true; - selectionRegionStart.x = DISPLAY_TO_REAL_POSITION_X(e.originalEvent.x); - selectionRegionStart.y = DISPLAY_TO_REAL_POSITION_Y(e.originalEvent.y-headerHeight); - selectionRegionEnd.x = selectionRegionStart.x; - selectionRegionEnd.y = selectionRegionStart.y; - } - - if (e.altKey){ - isDraggingWithAlt = true; - } else { - isDraggingWithAlt = false; - } - }); - - $("#logicalGraphD3Div svg").mousemove(function(e){ - - e.preventDefault() - if (!draggingInGraph){ - return - } - - if (isDraggingSelectionRegion){ - selectionRegionEnd.x = DISPLAY_TO_REAL_POSITION_X(e.originalEvent.x); - selectionRegionEnd.y = DISPLAY_TO_REAL_POSITION_Y(e.originalEvent.y-headerHeight); - } else { - // move background - eagle.globalOffsetX += e.originalEvent.movementX; - eagle.globalOffsetY += e.originalEvent.movementY; - hasDraggedBackground = true; - } - - tick(); - }) - - $("#logicalGraphD3Div svg").mouseup(function(e:any){ - if(e.button != 2){ - finishDragging(); - } - }) - - $("#logicalGraphD3Div svg").mouseleave(function(e:any){ - if(draggingInGraph === true){ - finishDragging(); - } - }) - - function finishDragging(){ - const hadPreviousSelection: boolean = eagle.selectedObjects().length > 0; - draggingInGraph = false; - - // if we just clicked on a node - if (!hasDraggedBackground && !isDraggingSelectionRegion){ - eagle.setSelection(eagle.rightWindow().mode(), null, Eagle.FileType.Unknown); - hasDraggedBackground = false; - if (hadPreviousSelection){ - eagle.rightWindow().mode(Eagle.RightWindowMode.Hierarchy); - } - } - - // if we dragged a selection region - if (isDraggingSelectionRegion){ - const nodes: Node[] = findNodesInRegion(selectionRegionStart.x, selectionRegionEnd.x, selectionRegionStart.y, selectionRegionEnd.y); - - const edges: Edge[] = findEdgesContainedByNodes(getEdges(eagle.logicalGraph(), eagle.showDataNodes()), nodes); - console.log("Found", nodes.length, "nodes and", edges.length, "edges in region"); - const objects: (Node | Edge)[] = []; - - // only add those objects which are not already selected - for (const node of nodes){ - if (!eagle.objectIsSelected(node)){ - objects.push(node); - } - } - for (const edge of edges){ - if (!eagle.objectIsSelected(edge)){ - objects.push(edge); - } - } - - objects.forEach(function(element){ - eagle.editSelection(Eagle.RightWindowMode.Hierarchy, element, Eagle.FileType.Graph ) - }) - - if (isDraggingWithAlt){ - for (const node of nodes){ - node.setCollapsed(false); - } - } - - selectionRegionStart.x = 0; - selectionRegionStart.y = 0; - selectionRegionEnd.x = 0; - selectionRegionEnd.y = 0; - - // finish selecting a region - isDraggingSelectionRegion = false; - - // necessary to make uncollapsed nodes show up - eagle.logicalGraph.valueHasMutated(); - } - } - - $("#logicalGraphD3Div svg").on("wheel", function(e:any){ - e.preventDefault() - // Somehow only the eagle.globalScale does something... - const wheelDelta = e.originalEvent.deltaY; - const zoomDivisor = Setting.findValue(Setting.GRAPH_ZOOM_DIVISOR); - - const xs = (e.clientX - eagle.globalOffsetX) / eagle.globalScale - const ys = (e.clientY - eagle.globalOffsetY) / eagle.globalScale - - eagle.globalScale *= (1-(wheelDelta/zoomDivisor)); - if(eagle.globalScale<0){ - eagle.globalScale = Math.abs(eagle.globalScale) - } - eagle.globalOffsetX = e.clientX - xs * eagle.globalScale; - eagle.globalOffsetY = e.clientY - ys * eagle.globalScale; - - tick(); - }); - - let nodes : any = rootContainer - .selectAll("g.node") - .data(nodeData) - .enter() - .append("g") - .attr("transform", nodeGetTranslation) - .attr("class", "node") - .attr("id", function(node : Node, index : number){return "node" + index;}) - .style("display", getNodeDisplay) - .on("contextmenu", function (d, i) { - d3.event.preventDefault(); - d3.event.stopPropagation(); - RightClick.initiateContextMenu(d,d3.event.target) - }); - - // rects - nodes - .append("rect") - .attr("width", function(node:Node){return getWidth(node);}) - .attr("height", function(node:Node){return getHeight(node);}) - .style("display", getNodeRectDisplay) - .style("fill", nodeGetFill) - .style("stroke", nodeGetStroke) - .style("stroke-width", NODE_STROKE_WIDTH) - .attr("stroke-dasharray", nodeGetStrokeDashArray); - - // custom-shaped nodes - nodes - .append("polygon") - .attr("points", getNodeCustomShapePoints) - .style("display", getNodeCustomShapeDisplay) - .style("fill", nodeGetColor) - .style("stroke", nodeGetStroke) - .style("stroke-width", NODE_STROKE_WIDTH) - .attr("stroke-dasharray", nodeGetStrokeDashArray); - - // update the parent of the given node - // however, if allGraphEditing is false, then don't update - // always keep track of whether an update would have happened, sp we can warn user - function _updateNodeParent(node: Node, parentKey: number, updated: {parent: boolean}, allowGraphEditing: boolean){ - if (node.getParentKey() !== parentKey){ - if (allowGraphEditing){ - node.setParentKey(parentKey); - } - updated.parent = true; - } - } - - const nodeDragHandler = d3 - .drag() - .on("start", function (node : Node) { - isDraggingNode = false; - dragEventCount = 0; - hasDraggedBackground = false; - - - if (d3.event.sourceEvent.shiftKey || d3.event.sourceEvent.altKey){ - hasDraggedBackground = true; - isDraggingSelectionRegion = true; - selectionRegionStart.x = DISPLAY_TO_REAL_POSITION_X(d3.event.sourceEvent.x); - selectionRegionStart.y = DISPLAY_TO_REAL_POSITION_Y(d3.event.sourceEvent.y-headerHeight); - selectionRegionEnd.x = selectionRegionStart.x; - selectionRegionEnd.y = selectionRegionStart.y; - return - } - - if (!eagle.objectIsSelected(node)){ - draggingNodeId = node.getId(); - } - - // new click time - const newTime = Date.now(); - const elapsedTime = newTime - Eagle.lastClickTime; - Eagle.lastClickTime = newTime; - - // check if this is a double click - if (elapsedTime < DOUBLE_CLICK_DURATION){ - node.toggleCollapsed(); - } - - // if node not selected, then select it - if (!eagle.objectIsSelected(node)){ - selectNode(node, d3.event.sourceEvent.shiftKey); - } - - // reset real - for (const node of eagle.logicalGraph().getNodes()){ - node.resetReal(); - } - - // record drag start position - dragStart.x = d3.event.sourceEvent.movementX; - dragStart.y = d3.event.sourceEvent.movementY; - - //tick(); - }) - .on("drag", function (node : Node, index : number) { - dragEventCount += 1; - - if (isDraggingSelectionRegion){ - selectionRegionEnd.x = DISPLAY_TO_REAL_POSITION_X(d3.event.sourceEvent.x); - selectionRegionEnd.y = DISPLAY_TO_REAL_POSITION_Y(d3.event.sourceEvent.y-headerHeight); - tick(); - return - } - - if (!isDraggingNode){ - isDraggingNode = true; - - if (d3.event.sourceEvent.altKey){ - isDraggingWithAlt = true; - } else { - isDraggingWithAlt = false; - } - } - - // get distance the mouse was moved - let movementX = d3.event.sourceEvent.movementX; - let movementY = d3.event.sourceEvent.movementY; - - // in testcafe, d3.event.sourceEvent.movementX and Y are always zero, use the d3.event.dx and dy instead - if (movementX === 0 && movementY === 0){ - movementX = d3.event.dx; - movementY = d3.event.dy; - - // avoid drag event 1 all together, it is too prone to huge movements - if (dragEventCount <=2){ - movementX = 0; - movementY = 0; - } - } - - //console.log(d3.event.sourceEvent.target.tagName, "dragEventCount", dragEventCount, "movementSource", movementSource, "movementX", movementX, "movementY", movementY); - - // transform change in x,y position using current scale factor - const dx = DISPLAY_TO_REAL_SCALE(movementX); - const dy = DISPLAY_TO_REAL_SCALE(movementY); - - // count number of nodes in the current selection - let numSelectedNodes = 0; - for (const object of eagle.selectedObjects()){ - if (object instanceof Node){ - numSelectedNodes += 1; - } - } - - // move all selected nodes, skip edges (they just follow nodes anyway) - for (const object of eagle.selectedObjects()){ - if (object instanceof Node){ - const actualChange = object.changePosition(dx, dy, numSelectedNodes === 1); - - if (!isDraggingWithAlt){ - moveChildNodes(object, dx, dy, actualChange.dx, actualChange.dy); - } - } - } - - // trigger updates - eagle.flagActiveFileModified(); - eagle.logicalGraph.valueHasMutated(); - }) - .on("end", function(node : Node){ - - // if we dragged a selection region - if (isDraggingSelectionRegion){ - const nodes: Node[] = findNodesInRegion(selectionRegionStart.x, selectionRegionEnd.x, selectionRegionStart.y, selectionRegionEnd.y); - - //checking if there was no drag distance, if so we are clicking a single object and we will toggle its seletion - if(Math.abs(selectionRegionStart.x-selectionRegionEnd.x)+Math.abs(selectionRegionStart.y - selectionRegionEnd.y)<3){ - eagle.editSelection(Eagle.RightWindowMode.Inspector, node,Eagle.FileType.Graph); - return - } - - const edges: Edge[] = findEdgesContainedByNodes(getEdges(eagle.logicalGraph(), eagle.showDataNodes()), nodes); - console.log("Found", nodes.length, "nodes and", edges.length, "edges in region"); - const objects: (Node | Edge)[] = []; - - // only add those objects which are not already selected - for (const node of nodes){ - if (!eagle.objectIsSelected(node)){ - objects.push(node); - } - } - for (const edge of edges){ - if (!eagle.objectIsSelected(edge)){ - objects.push(edge); - } - } - - objects.forEach(function(element){ - eagle.editSelection(Eagle.RightWindowMode.Hierarchy, element, Eagle.FileType.Graph ) - }) - - if (isDraggingWithAlt){ - for (const node of nodes){ - node.setCollapsed(false); - } - } - - selectionRegionStart.x = 0; - selectionRegionStart.y = 0; - selectionRegionEnd.x = 0; - selectionRegionEnd.y = 0; - - // finish selecting a region - isDraggingSelectionRegion = false; - - // necessary to make uncollapsed nodes show up - eagle.logicalGraph.valueHasMutated(); - return - } - - // update location (in real node data, not sortedData) - // guarding this behind 'isDraggingNode' is a hack to get around the fact that d3.event.x and d3.event.y behave strangely - if (isDraggingNode){ - isDraggingNode = false; - draggingNodeId = ""; - } else { - // if node already selected, then deselect it - if (eagle.objectIsSelected(node) && draggingNodeId !== node.getId()){ - selectNode(node, d3.event.sourceEvent.shiftKey); - } - } - - // determine the size of the node being moved, based on whether it is collapsed or not - let posX, posY, width, height = 0; - if (node.isCollapsed()){ - // find center of node - const centerX = node.getPosition().x + node.getWidth()/2; - const centerY = node.getPosition().y + node.getHeight()/2; - - // top left corner of icon - posX = centerX - Node.DATA_COMPONENT_WIDTH/2; - posY = centerY - Node.DATA_COMPONENT_HEIGHT/2; - width = Node.DATA_COMPONENT_WIDTH; - height = Node.DATA_COMPONENT_HEIGHT; - } else { - posX = node.getPosition().x; - posY = node.getPosition().y; - width = node.getWidth(); - height = node.getHeight(); - } - - // check for nodes underneath the node we dropped - const parent : Node = eagle.logicalGraph().checkForNodeAt(posX, posY, width, height, node.getKey(), true); - - // check if new candidate parent is already a descendent of the node, this would cause a circular hierarchy which would be bad - const ancestorOfParent = isAncestor(parent, node); - - // keep track of whether we would update any node parents - const updated = {parent: false}; - const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING); - - // if a parent was found, update - if (parent !== null && node.getParentKey() !== parent.getKey() && node.getKey() !== parent.getKey() && !ancestorOfParent){ - //console.log("set parent", parent.getKey()); - //node.setParentKey(parent.getKey()); - _updateNodeParent(node, parent.getKey(), updated, allowGraphEditing); - } - - // if no parent found, update - if (parent === null && node.getParentKey() !== null){ - //console.log("set parent", null); - //node.setParentKey(null); - _updateNodeParent(node, null, updated, allowGraphEditing); - } - - // also check that to see if current children are still in within the group - if (isDraggingWithAlt && node.isGroup() && !node.isCollapsed()){ - // loop through all nodes, check if node is a child - // if so, run checkForNodeAt and make sure result is parent - for (let i = 0; i < nodeData.length ; i++){ - const child : Node = nodeData[i]; - - if (child.getParentKey() === node.getKey()){ - const parent : Node = eagle.logicalGraph().checkForNodeAt(child.getPosition().x, child.getPosition().y, child.getWidth(), child.getHeight(), child.getKey(), true); - - // un-parent the child if no longer contained within the node we are dragging - if (parent === null || parent.getKey() !== node.getKey()){ - //child.setParentKey(null); - _updateNodeParent(child, null, updated, allowGraphEditing); - } - } - } - } - - eagle.undo().pushSnapshot(eagle, "Move node " + node.getName()); - - if (!allowGraphEditing && updated.parent){ - Utils.showNotification("Node Parent not Changed", "Graph Editing is disabled", "danger"); - } - - //tick(); - }); - - nodeDragHandler(rootContainer.selectAll("g.node")); - - const customTriangle = d3.symbol().type(d3.symbolTriangle) - - // add a header background to each node - nodes - .append("rect") - .attr("class", "header-background") - .attr("width", function(node:Node){return getHeaderBackgroundWidth(node);}) - .attr("height", function(node:Node){return getHeaderBackgroundHeight(node);}) - .attr("x", HEADER_INSET) - .attr("y", HEADER_INSET) - .style("fill", nodeGetColor) - .style("stroke", "grey") - .style("display", getHeaderBackgroundDisplay); - - // add a text header to each node - nodes - .append("foreignObject") - .attr("class", "header-icon") - .style("width", "40px") - .style("height", "40px") - .style("pointer-events", "none") - .style("display", "inline") - .style("font-size", '20px') - .style("color", "white") - .attr("x", "5px") - .attr("y", "2px") - .append("xhtml:span") - .attr("class", function(node:Node){ - if (node.isGroup()){ - return node.getIcon() - }else{ - return "" - } - }) - - // add a text header to each node - nodes - .append("text") - .attr("class", "header") - .attr("x", function(node:Node){return getHeaderPositionX(node);}) - .attr("y", function(node:Node){return getHeaderPositionY(node);}) - .attr("eagle-wrap-width", getWrapWidth) - .style("fill", getHeaderFill) - .style("font-size", HEADER_TEXT_FONT_SIZE + "px") - .style("font-weight", getHeaderFontWeight) - .style("display", getHeaderDisplay) - .text(getHeaderText) - .call(wrap, false); - - // add a app names background to each node - nodes - .append("rect") - .attr("class", "apps-background") - .attr("width", function(node:Node){return getAppsBackgroundWidth(node);}) - .attr("height", function(node:Node){return getAppsBackgroundHeight(node);}) - .attr("x", HEADER_INSET) - .attr("y", function(node:Node){return HEADER_INSET + getHeaderBackgroundHeight(node);}) - .style("fill", nodeGetColor) - .style("stroke", "grey") - .style("display", getAppsBackgroundDisplay); - - // add the input name text - nodes - .append("text") - .attr("class", "inputAppName") - .attr("x", function(node:Node){return getInputAppPositionX(node);}) - .attr("y", function(node:Node){return getInputAppPositionY(node);}) - .style("fill", getHeaderFill) - .style("font-size", HEADER_TEXT_FONT_SIZE + "px") - .style("display", getAppsBackgroundDisplay) - .text(getInputAppText) - .on("contextmenu", function (d:Node, i:number) { - d3.event.preventDefault(); - // d3.event.stopPropagation(); - RightClick.initiateContextMenu(d.getInputApplication(),d3.event.target) - }); - - // add the output name text - nodes - .append("text") - .attr("class", "outputAppName") - .attr("x", function(node:Node){return getOutputAppPositionX(node);}) - .attr("y", function(node:Node){return getOutputAppPositionY(node);}) - .style("fill", getHeaderFill) - .style("font-size", HEADER_TEXT_FONT_SIZE + "px") - .style("display", getAppsBackgroundDisplay) - .text(getOutputAppText) - .on("contextmenu", function (d:Node, i:number) { - d3.event.preventDefault(); - // d3.event.stopPropagation(); - RightClick.initiateContextMenu(d.getOutputApplication(),d3.event.target) - }); - - // add the content text - nodes - .append("text") - .attr("class", "content") - .attr("x", function(node:Node){return getContentPositionX(node);}) - .attr("y", function(node:Node){return getContentPositionY(node);}) - .attr("eagle-wrap-width", getWrapWidth) - .style("fill", getContentFill) - .style("font-size", CONTENT_TEXT_FONT_SIZE + "px") - .style("display", getContentDisplay) - .text(getContentText) - .call(wrap, true); - - // add the svg icon - nodes - .append('foreignObject') - .attr("class","nodeIcon") - .attr("width", Node.DATA_COMPONENT_WIDTH+4) - .attr("height", Node.DATA_COMPONENT_HEIGHT+4) - .attr("x", function(node:Node){return getIconLocationX(node);}) - .attr("y", function(node:Node){return getIconLocationY(node);}) - .style("display", getIconDisplay) - .attr('data-bind','eagleRightClick: $data') - .append('xhtml:div') - .attr("style", function(node:Node){if (eagle.objectIsSelected(node) && node.isCollapsed() && !node.isPeek()){return "background-color:lightgrey; border-radius:4px; border:2px solid "+Config.SELECTED_NODE_COLOR+"; padding:2px; transform:scale(.9);line-height: normal;"}else{return "line-height: normal;padding:4px;transform:scale(.9);"}}) - .append('xhtml:span') - .attr("style", function(node:Node){ return node.getGraphIconAttr()}) - .attr("class", function(node:Node){return node.getIcon();}); - - // add the resize controls - nodes - .append("rect") - .attr("class", "resize-control") - .attr("width", RESIZE_CONTROL_SIZE) - .attr("height", RESIZE_CONTROL_SIZE) - .attr("x", function(node : Node){return getWidth(node) - RESIZE_CONTROL_SIZE;}) - .attr("y", function(node : Node){return getHeight(node) - RESIZE_CONTROL_SIZE;}) - .style("display", getResizeControlDisplay); - - // add the resize labels - nodes - .append("text") - .attr("class", "resize-control-label") - .attr('x', function(node : Node){return getWidth(node) - RESIZE_CONTROL_SIZE;}) - .attr('y', function(node : Node){return getHeight(node) - 2;}) - .style('font-size', RESIZE_BUTTON_LABEL_FONT_SIZE + 'px') - .style('display', getResizeControlDisplay) - .style('user-select', 'none') - .style('cursor', 'nwse-resize') - .text(RESIZE_BUTTON_LABEL); - - const resizeDragHandler = d3 - .drag() - .on("start", function (node : Node) { - selectNode(node, false); - tick(); - }) - .on("drag", function (node : Node) { - let newWidth = node.getWidth() + DISPLAY_TO_REAL_SCALE(d3.event.sourceEvent.movementX); - let newHeight = node.getHeight() + DISPLAY_TO_REAL_SCALE(d3.event.sourceEvent.movementY); - - // ensure node are of at least a minimum size - newWidth = Math.max(newWidth, Node.MINIMUM_WIDTH); - newHeight = Math.max(newHeight, Node.MINIMUM_HEIGHT); - - node.setWidth(newWidth); - node.setHeight(newHeight); - - eagle.logicalGraph.valueHasMutated(); - //tick(); - }); - - resizeDragHandler(rootContainer.selectAll("g.node rect.resize-control")); - resizeDragHandler(rootContainer.selectAll("g.node text.resize-control-label")); - - const inputAppDragHandler = d3.drag() - .on("end", function (node: Node){ - // check if node has an input app - if (node.hasInputApplication()){ - eagle.setSelection(Eagle.RightWindowMode.Inspector, node.getInputApplication(), Eagle.FileType.Graph); - } else { - eagle.setNodeInputApplication(node.getKey()); - } - }); - - const outputAppDragHandler = d3.drag() - .on("end", function (node: Node){ - // check if node has an output app - if (node.hasOutputApplication()){ - eagle.setSelection(Eagle.RightWindowMode.Inspector, node.getOutputApplication(), Eagle.FileType.Graph); - } else { - eagle.setNodeOutputApplication(node.getKey()); - } - }); - - inputAppDragHandler(rootContainer.selectAll("g.node text.inputAppName")); - outputAppDragHandler(rootContainer.selectAll("g.node text.outputAppName")); - - // add shrink buttons - nodes - .append("rect") - .attr("class", "shrink-button") - .attr("width", SHRINK_BUTTON_SIZE) - .attr("height", SHRINK_BUTTON_SIZE) - .attr("x", function(node : Node){return getWidth(node) - SHRINK_BUTTON_SIZE - HEADER_INSET - 4;}) - .attr("y", HEADER_INSET + 4) - .style("display", getShrinkControlDisplay) - .on("click", shrinkOnClick); - - // add shrink button labels - nodes - .append("text") - .attr("class", "shrink-button-label") - .attr('x', function(node : Node){return getWidth(node) - SHRINK_BUTTON_SIZE - HEADER_INSET - 2;}) - .attr('y', HEADER_INSET + 8 + (SHRINK_BUTTON_SIZE/2)) - .style('font-size', HEADER_BUTTON_LABEL_FONT_SIZE + 'px') - .style('fill', 'black') - .style('display', getShrinkControlDisplay) - .style('user-select', 'none') - .text(SHRINK_BUTTON_LABEL) - .on("click", shrinkOnClick); - - // add the left-side ports (by default, the input ports) - const inputPortGroups = nodes - .append("g") - .attr("class", getInputPortGroupClass) - .attr("transform", getInputPortGroupTransform) - .style("display", getPortsDisplay); - - // add input ports - inputPortGroups - .selectAll("g") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .enter() - .append("text") - .attr("class", getPortClass) - .attr("x", getInputPortPositionX) - .attr("y", getInputPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - const inputCircles = inputPortGroups - .selectAll("g") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .enter() - .append("circle") - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "hiddenPortIcon" : ""}) - .attr("cx", getInputPortCirclePositionX) - .attr("cy", getInputPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - const inputTriangles = inputPortGroups - .selectAll("g") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .enter() - .append("path") - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "" : "hiddenPortIcon"}) - .attr("style", getInputPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // add the input local ports - const inputLocalPortGroups = nodes - .append("g") - .attr("class", getInputLocalPortGroupClass) - .attr("transform", getInputLocalPortGroupTransform) - .style("display", getPortsDisplay); - - inputLocalPortGroups - .selectAll("g") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .enter() - .append("text") - .attr("class", function(port : Field){return port.getIsEvent() ? "event" : ""}) - .attr("x", getInputLocalPortPositionX) - .attr("y", getInputLocalPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - const inputLocalCircles = inputLocalPortGroups - .selectAll("g") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .enter() - .append("circle") - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "hiddenPortIcon" : ""}) - .attr("cx", getInputLocalPortCirclePositionX) - .attr("cy", getInputLocalPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - const inputLocalTriangles = inputLocalPortGroups - .selectAll("g") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .enter() - .append("path") - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "" : "hiddenPortIcon"}) - .attr("style", getInputLocalPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // add the output ports - const outputPortGroups = nodes - .append("g") - .attr("class", getOutputPortGroupClass) - .attr("transform", getOutputPortGroupTransform) - .style("display", getPortsDisplay); - - outputPortGroups - .selectAll("g") - .data(function(node : Node, index : number){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .enter() - .append("text") - .attr("class", getPortClass) - .attr("x", getOutputPortPositionX) - .attr("y", getOutputPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - const outputCircles = outputPortGroups - .selectAll("g") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .enter() - .append("circle") - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "hiddenPortIcon" : ""}) - .attr("cx", getOutputPortCirclePositionX) - .attr("cy", getOutputPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - const outputTriangles = outputPortGroups - .selectAll("g") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .enter() - .append("path") - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "" : "hiddenPortIcon"}) - .attr("style", getOutputPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // add the output local ports - const outputLocalPortGroups = nodes - .append("g") - .attr("class", getOutputLocalPortGroupClass) - .attr("transform", getOutputLocalPortGroupTransform) - .style("display", getPortsDisplay); - - outputLocalPortGroups - .selectAll("g") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .enter() - .append("text") - .attr("class", function(port : Field){return port.getIsEvent() ? "event" : ""}) - .attr("x", getOutputLocalPortPositionX) - .attr("y", getOutputLocalPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - const outputLocalCircles = outputLocalPortGroups - .selectAll("g") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .enter() - .append("circle") - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "hiddenPortIcon" : ""}) - .attr("cx", getOutputLocalPortCirclePositionX) - .attr("cy", getOutputLocalPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - const outputLocalTriangles = outputLocalPortGroups - .selectAll("g") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .enter() - .append("path") - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "" : "hiddenPortIcon"}) - .attr("style", getOutputLocalPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - const portDragHandler = d3.drag() - .on("start", function (port : Field) { - //console.log("drag start", "nodeKey", port.getNodeKey(), "portId", port.getId(), "portName", port.getDisplayText()); - isDraggingPort = true; - sourceNode = graph.findNodeByKey(port.getNodeKey()); - sourcePort = port; - sourcePortIsInput = d3.event.sourceEvent.target.dataset.usage === "input"; - }) - .on("drag", function () { - //console.log("drag from port", data.Id); - mousePosition.x = d3.mouse(svgContainer.node())[0]; - mousePosition.y = d3.mouse(svgContainer.node())[1]; - - // convert mouse position to graph coordinates - const mouseX = DISPLAY_TO_REAL_POSITION_X(mousePosition.x); - const mouseY = DISPLAY_TO_REAL_POSITION_Y(mousePosition.y); - - // check for nearby nodes - const nearbyNodes = findNodesInRange(mouseX, mouseY, MIN_AUTO_COMPLETE_EDGE_RANGE, sourceNode.getKey()); - - // check for nearest matching port in the nearby nodes - const matchingPort: Field = findNearestMatchingPort(mouseX, mouseY, nearbyNodes, sourceNode, sourcePort, sourcePortIsInput); - - if (matchingPort !== null){ - suggestedNode = graph.findNodeByKey(matchingPort.getNodeKey()); - suggestedPort = matchingPort; - } else { - suggestedNode = null; - suggestedPort = null; - } - - // reset all nodes to peek false - for (const node of nodeData){ - node.setPeek(false); - } - - // peek at the suggestedNode, if one exists - if (suggestedNode){ - suggestedNode.setPeek(true); - } - - tick(); - }) - .on("end", function(port : Field){ - //console.log("drag end", port.getId()); - isDraggingPort = false; - - if (destinationPort !== null || suggestedPort !== null){ - const srcNode = sourceNode; - const srcPort = sourcePort; - - let destNode; - let destPort; - - if (destinationPort !== null){ - destNode = destinationNode; - destPort = destinationPort; - } else { - destNode = suggestedNode; - destPort = suggestedPort; - } - - // check if edge is back-to-front (input-to-output), if so, swap the source and destination - //const backToFront : boolean = (srcPortType === "input" || srcPortType === "outputLocal") && (destPortType === "output" || destPortType === "inputLocal"); - const backToFront : boolean = sourcePortIsInput; - const realSourceNode: Node = backToFront ? destNode : srcNode; - const realSourcePort: Field = backToFront ? destPort : srcPort; - const realDestinationNode: Node = backToFront ? srcNode : destNode; - const realDestinationPort: Field = backToFront ? srcPort : destPort; - - // notify user - if (backToFront){ - Utils.showNotification("Automatically reversed edge direction", "The edge began at an input port and ended at an output port, so the direction was reversed.", "info"); - } - - // check if link is valid - const linkValid : Eagle.LinkValid = Edge.isValid(eagle, null, realSourceNode.getKey(), realSourcePort.getId(), realDestinationNode.getKey(), realDestinationPort.getId(), realSourcePort.getType(), false, false, true, true, {errors:[], warnings:[]}); - - // abort if edge is invalid - if (Setting.findValue(Setting.ALLOW_INVALID_EDGES) || linkValid === Eagle.LinkValid.Valid || linkValid === Eagle.LinkValid.Warning){ - if (linkValid === Eagle.LinkValid.Warning){ - addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, true, false); - } else { - addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, false, false); - } - } else { - console.warn("link not valid, result", linkValid); - } - } else { - // no destination, ask user to choose a new node - const dataEligible = sourceNode.getCategoryType() !== Category.Type.Data; - //getting matches from both the graph and the palettes list - const eligibleComponents = Utils.getComponentsWithMatchingPort('palette graph', !sourcePortIsInput, sourcePort.getType(), dataEligible); - - // console.log("Found", eligibleComponents.length, "eligible automatically suggested components that have a " + (sourcePortIsInput ? "output" : "input") + " port of type:", sourcePort.getType()); - - // check we found at least one eligible component - if (eligibleComponents.length === 0){ - Utils.showNotification("Not Found", "No eligible components found for connection to port of this type (" + sourcePort.getType() + ")", "info"); - } else { - - // get list of strings from list of eligible components - const eligibleComponentNames : Node[] = []; - for (const c of eligibleComponents){ - eligibleComponentNames.push(c); - } - - // NOTE: create copy in right click ts because we are using the right click menus to handle the node selection - RightClick.edgeDropSrcNode = sourceNode; - RightClick.edgeDropSrcPort = sourcePort; - RightClick.edgeDropSrcIsInput = sourcePortIsInput; - - const x = DISPLAY_TO_REAL_POSITION_X(mousePosition.x); - const y = DISPLAY_TO_REAL_POSITION_Y(mousePosition.y); - - Eagle.selectedRightClickPosition = {x:x, y:y}; - - RightClick.edgeDropCreateNode(eligibleComponentNames,null) - } - } - - // stop peeking at any nodes - for (const node of nodeData){ - node.setPeek(false); - } - - clearEdgeVars(); - eagle.logicalGraph.valueHasMutated(); - }); - - portDragHandler(inputCircles); - portDragHandler(inputLocalCircles); - portDragHandler(outputCircles); - portDragHandler(outputLocalCircles); - portDragHandler(inputTriangles); - portDragHandler(inputLocalTriangles); - portDragHandler(outputTriangles); - portDragHandler(outputLocalTriangles); - - // draw link extras (these a invisble wider links that assist users in selecting the edges) - // TODO: ideally we would not use the 'any' type here - const linkExtras : any = rootContainer - .selectAll("path.linkExtra") - .data(linkData) - .enter() - .append("path") - .on("contextmenu", function (linkData, i) { - d3.event.preventDefault(); - d3.event.stopPropagation(); - RightClick.initiateContextMenu(linkData,d3.event.target) - }); - - - linkExtras - .attr("class", "linkExtra") - .attr("d", createLink) - .attr("stroke", "transparent") - .attr("stroke-width", "10px") - .attr("fill", "none") - .attr("style","pointer-events:visible-stroke;") - .style("display", getEdgeDisplay); - - // draw links - // TODO: ideally we would not use the 'any' type here - let links : any = rootContainer - .selectAll("path.link") - .data(linkData) - .enter() - .append("path"); - - - links - .attr("class", "link") - .attr("d", createLink) - .attr("stroke", edgeGetStrokeColor) - .attr("stroke-dasharray", edgeGetStrokeDashArray) - .attr("fill", "none") - .attr("marker-end", edgeGetArrowheadUrl) - .style("display", getEdgeDisplay) - - - const edgeDragHandler = d3 - .drag() - .on("start", function(edge : Edge){ - selectEdge(edge, d3.event.sourceEvent.shiftKey); - tick(); - }); - - edgeDragHandler(rootContainer.selectAll("path.link, path.linkExtra")); - - - // draw comment links - let commentLinks : any = rootContainer - .selectAll("path.commentLink") - .data(nodeData) - .enter() - .append("path"); - - commentLinks - .attr("class", "commentLink") - .attr("d", createCommentLink) - .attr("stroke", LINK_COLORS.DEFAULT) - .attr("fill", "transparent") - .attr("marker-end", "url(#DEFAULT)") - .style("display", getCommentLinkDisplay); - - // create one link that is only used during the creation of a new link - // this new link follows the mouse pointer to indicate the position - const draggingLink = rootContainer - .append("line") - .attr("class", "draggingLink") - .attr("x1", 0) - .attr("y1", 0) - .attr("x2", 0) - .attr("y2", 0) - .attr("stroke", draggingEdgeGetStrokeColor); - - // create one link that is only used during the creation of a new link - // this new link suggests to the user the edge suggested by the auto-complete function - const autoCompleteLink = rootContainer - .append("line") - .attr("class", "autoCompleteLink") - .attr("x1", 0) - .attr("y1", 0) - .attr("x2", 0) - .attr("y2", 0) - .attr("stroke", LINK_COLORS.AUTO_COMPLETE); - - const selectionRegion = rootContainer - .append("rect") - .attr("class", "selection-region") - .attr("width", 0) - .attr("height", 0) - .attr("x", 0) - .attr("y", 0) - .attr("stroke", "black") - .attr("fill", "transparent") - .style("display", "inline"); - - function determineDirection(source: boolean, node: Node, portIndex: number, portType: Daliuge.FieldUsage): Eagle.Direction { - if (source){ - if (node.isBranch()){ - if (portIndex === 0){ - return Eagle.Direction.Down; - } - if (portIndex === 1){ - return Eagle.Direction.Right; - } - } - - if (portType === Daliuge.FieldUsage.OutputPort || portType === Daliuge.FieldUsage.InputOutput){ - return node.isFlipPorts() ? Eagle.Direction.Left : Eagle.Direction.Right; - } else { - return node.isFlipPorts() ? Eagle.Direction.Right : Eagle.Direction.Left; - } - } else { - if (node.isBranch()){ - if (portIndex === 0){ - return Eagle.Direction.Down; - } - if (portIndex === 1){ - return Eagle.Direction.Right; - } - } - - if (portType === Daliuge.FieldUsage.InputPort || portType === Daliuge.FieldUsage.InputOutput){ - return node.isFlipPorts() ? Eagle.Direction.Left : Eagle.Direction.Right; - } else { - return node.isFlipPorts() ? Eagle.Direction.Right : Eagle.Direction.Left; - } - } - } - - function createLink(edge : Edge) : string { - // determine if edge is "forward" or not - const srcNode : Node = findNodeWithKey(edge.getSrcNodeKey(), nodeData); - const destNode : Node = findNodeWithKey(edge.getDestNodeKey(), nodeData); - - if (srcNode === null || destNode === null){ - console.warn("Can't find srcNode or can't find destNode for edge."); - return createBezier(0,0,0,0,Eagle.Direction.Down,Eagle.Direction.Down, edge.isClosesLoop()); - } - - const srcPort : Field = srcNode.findFieldById(edge.getSrcPortId()); - const destPort : Field = destNode.findFieldById(edge.getDestPortId()); - - if (srcPort === null){ - console.warn("Can't find srcPort (" + edge.getSrcPortId() + ") on srcNode (" + srcNode.getName() + ") for edge (" + edge.getId() + ")."); - } - - if (destPort === null){ - console.warn("Can't find destPort (" + edge.getDestPortId() + ") on destNode (" + destNode.getName() + ") for edge (" + edge.getId() + ")."); - } - - if (srcPort === null || destPort === null){ - return createBezier(0,0,0,0,Eagle.Direction.Down,Eagle.Direction.Down, edge.isClosesLoop()); - } - - const srcPortType : Daliuge.FieldUsage = srcPort.getUsage(); - const destPortType : Daliuge.FieldUsage = destPort.getUsage(); - const srcPortIndex : number = srcNode.findPortIndexById(edge.getSrcPortId()); - const destPortIndex : number = destNode.findPortIndexById(edge.getDestPortId()); - - const srcPortPos = findNodePortPosition(srcNode, edge.getSrcPortId(), false, false); - const destPortPos = findNodePortPosition(destNode, edge.getDestPortId(), true, false); - - let x1 = srcPortPos.x; - let y1 = srcPortPos.y; - let x2 = destPortPos.x; - let y2 = destPortPos.y; - - console.assert(!isNaN(x1), "Source x-coord of edge cannot be found: " + srcNode.getName() + " -> " + destNode.getName()); - console.assert(!isNaN(y1), "Source y-coord of edge cannot be found: " + srcNode.getName() + " -> " + destNode.getName()); - console.assert(!isNaN(x2), "Destination x-coord of edge cannot be found: " + srcNode.getName() + " -> " + destNode.getName()); - console.assert(!isNaN(y2), "Destination y-coord of edge cannot be found: " + srcNode.getName() + " -> " + destNode.getName()); - - // if coordinate isNaN, replace with a default, so at least the edge can be drawn - if (isNaN(x1)) x1 = 0; - if (isNaN(y1)) y1 = 0; - if (isNaN(x2)) x2 = 0; - if (isNaN(y2)) y2 = 0; - - const startDirection = determineDirection(true, srcNode, srcPortIndex, srcPortType); - const endDirection = determineDirection(false, destNode, destPortIndex, destPortType); - //console.log("edge", edge.getId(), "startDir", startDirection, "endDir", endDirection, "srcPortType", srcPortType, "destPortType", destPortType); - - return createBezier(x1, y1, x2, y2, startDirection, endDirection, edge.isClosesLoop()); - } - - function getEdges(graph: LogicalGraph, showDataNodes: boolean): Edge[]{ - if (showDataNodes){ - return graph.getEdges(); - } else { - //return [graph.getEdges()[0]]; - const edges: Edge[] = []; - - for (const edge of graph.getEdges()){ - let srcHasConnectedInput: boolean = false; - let destHasConnectedOutput: boolean = false; - - for (const e of graph.getEdges()){ - if (e.getDestNodeKey() === edge.getSrcNodeKey()){ - srcHasConnectedInput = true; - } - if (e.getSrcNodeKey() === edge.getDestNodeKey()){ - destHasConnectedOutput = true; - } - } - - const srcIsDataNode: boolean = findNodeWithKey(edge.getSrcNodeKey(), graph.getNodes()).isData(); - const destIsDataNode: boolean = findNodeWithKey(edge.getDestNodeKey(), graph.getNodes()).isData(); - //console.log("edge", edge.getId(), "srcIsDataNode", srcIsDataNode, "srcHasConnectedInput", srcHasConnectedInput, "destIsDataNode", destIsDataNode, "destHasConnectedOutput", destHasConnectedOutput); - - if (destIsDataNode){ - if (!destHasConnectedOutput){ - // draw edge as normal - edges.push(edge); - } - continue; - } - - if (srcIsDataNode){ - if (srcHasConnectedInput){ - // build a new edge - const newSrc = findInputToDataNode(graph.getEdges(), edge.getSrcNodeKey()); - edges.push(new Edge(newSrc.nodeKey, newSrc.portId, edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false)); - } else { - // draw edge as normal - edges.push(edge); - } - } - } - - return edges; - } - } - - function findInputToDataNode(edges: Edge[], nodeKey: number) : {nodeKey:number, portId: string}{ - for (const edge of edges){ - if (edge.getDestNodeKey() === nodeKey){ - return { - nodeKey: edge.getSrcNodeKey(), - portId: edge.getSrcPortId() - }; - } - } - - return null; - } - - function tick(){ - const startTime = performance.now(); - eagle.rendererFrameCountTick = eagle.rendererFrameCountTick + 1; - - - // background (and grid if enabled) - const rectWidth = $('#logicalGraphD3Div').width(); - const rectHeight = $('#logicalGraphD3Div').height(); - const offsetX = -eagle.globalOffsetX / eagle.globalScale; - const offsetY = -eagle.globalOffsetY / eagle.globalScale; - - rootContainer - .selectAll("rect.background") - .attr("fill", eagle.snapToGrid() ? "url(#grid)" : "transparent") - .attr("x", offsetX) - .attr("y", offsetY) - .attr("width", rectWidth*10) - .attr("height", rectHeight*10); - - // scale the root node - rootContainer - .attr("transform", rootScaleTranslation); - - // enter any new nodes - rootContainer - .selectAll("g.node") - .data(nodeData) - .enter() - .insert("g") - .attr("class", "node") - .attr("id", function(node : Node, index : number){return "node" + index;}); - - // exit any old nodes - rootContainer - .selectAll("g.node") - .data(nodeData) - .exit() - .remove(); - - // enter any new links - rootContainer - .selectAll("path.linkExtra") - .data(linkData) - .enter() - .insert("path") - .attr("class", "linkExtra") - .style("display", getEdgeDisplay) - - // exit any old links. - rootContainer - .selectAll("path.linkExtra") - .data(linkData) - .exit() - .remove(); - - // enter any new links - rootContainer - .selectAll("path.link") - .data(linkData) - .enter() - .insert("path") - .attr("class", "link") - .style("display", getEdgeDisplay) - - // exit any old links. - rootContainer - .selectAll("path.link") - .data(linkData) - .exit() - .remove(); - - // enter any new comment links - rootContainer - .selectAll("path.commentLink") - .data(nodeData) - .enter() - .insert("path") - .attr("class", "commentLink") - .style("display", getCommentLinkDisplay); - - // exit any old comment links - rootContainer - .selectAll("path.commentLink") - .data(nodeData) - .exit() - .remove(); - - // make sure we have references to all the objects of each type - nodes = rootContainer - .selectAll("g.node") - .data(nodeData) - .style("display", getNodeDisplay); - links = rootContainer - .selectAll("path.link") - .data(linkData); - commentLinks = rootContainer - .selectAll("path.commentLink") - .data(nodeData); - - // TODO: update attributes of all nodes - nodes.attr("transform", nodeGetTranslation); - - rootContainer - .selectAll("g.node rect:not(.header-background):not(.apps-background):not(.resize-control):not(.shrink-button)") - .data(nodeData) - .attr("width", function(node:Node){return getWidth(node);}) - .attr("height", function(node:Node){return getHeight(node);}) - .style("display", getNodeRectDisplay) - .style("fill", nodeGetFill) - .style("stroke", nodeGetStroke) - .style("stroke-width", NODE_STROKE_WIDTH) - .attr("stroke-dasharray", nodeGetStrokeDashArray); - - rootContainer - .selectAll("g.node polygon") - .data(nodeData) - .attr("points", getNodeCustomShapePoints) - .style("display", getNodeCustomShapeDisplay) - .style("fill", nodeGetColor) - .style("stroke", nodeGetStroke) - .style("stroke-width", NODE_STROKE_WIDTH) - .attr("stroke-dasharray", nodeGetStrokeDashArray); - - rootContainer - .selectAll("g.node rect.header-background") - .data(nodeData) - .attr("width", function(node:Node){return getHeaderBackgroundWidth(node);}) - .attr("height", function(node:Node){return getHeaderBackgroundHeight(node);}) - .attr("x", HEADER_INSET) - .attr("y", HEADER_INSET) - .style("fill", nodeGetColor) - .style("stroke", "grey") - .style("display", getHeaderBackgroundDisplay); - - rootContainer - .selectAll("g.node foreignObject.header-icon") - .data(nodeData) - .style("width", "40px") - .style("pointer-events", "none") - .style("height", "40px") - .style("display", "inline") - .style("font-size", '20px') - .style("color", "white") - .attr("x", "5px") - .attr("y", "2px") - - rootContainer - .selectAll("g.node text.header") - .data(nodeData) - .attr("x", function(node:Node){return getHeaderPositionX(node);}) - .attr("y", function(node:Node){return getHeaderPositionY(node);}) - .attr("eagle-wrap-width", getWrapWidth) - .style("fill", getHeaderFill) - .style("font-size", HEADER_TEXT_FONT_SIZE + "px") - .style("font-weight", getHeaderFontWeight) - .style("display", getHeaderDisplay) - .text(getHeaderText) - .call(wrap, false); - - rootContainer - .selectAll("g.node rect.apps-background") - .data(nodeData) - .attr("width", function(node:Node){return getAppsBackgroundWidth(node);}) - .attr("height", function(node:Node){return getAppsBackgroundHeight(node);}) - .attr("x", HEADER_INSET) - .attr("y", function(node:Node){return HEADER_INSET + getHeaderBackgroundHeight(node);}) - .style("fill", nodeGetColor) - .style("stroke", "grey") - .style("display", getAppsBackgroundDisplay); - - rootContainer - .selectAll("g.node text.inputAppName") - .data(nodeData) - .attr("x", function(node:Node){return getInputAppPositionX(node);}) - .attr("y", function(node:Node){return getInputAppPositionY(node);}) - .style("fill", getHeaderFill) - .style("font-size", HEADER_TEXT_FONT_SIZE + "px") - .style("display", getAppsBackgroundDisplay) - .text(getInputAppText) - .on("contextmenu", function (d:Node, i:number) { - d3.event.preventDefault(); - d3.event.stopPropagation(); - RightClick.initiateContextMenu(d.getInputApplication(),d3.event.target) - }); - - rootContainer - .selectAll("g.node text.outputAppName") - .data(nodeData) - .attr("x", function(node:Node){return getOutputAppPositionX(node);}) - .attr("y", function(node:Node){return getOutputAppPositionY(node);}) - .style("fill", getHeaderFill) - .style("font-size", HEADER_TEXT_FONT_SIZE + "px") - .style("display", getAppsBackgroundDisplay) - .text(getOutputAppText) - .on("contextmenu", function (d:Node, i:number) { - d3.event.preventDefault(); - d3.event.stopPropagation(); - RightClick.initiateContextMenu(d.getOutputApplication(),d3.event.target) - }); - - rootContainer - .selectAll("g.node text.content") - .data(nodeData) - .attr("x", function(node:Node){return getContentPositionX(node);}) - .attr("y", function(node:Node){return getContentPositionY(node);}) - .attr("eagle-wrap-width", getWrapWidth) - .style("fill", getContentFill) - .style("font-size", CONTENT_TEXT_FONT_SIZE + "px") - .style("display", getContentDisplay) - .text(getContentText) - .call(wrap, true); - - rootContainer - .selectAll("g.node foreignObject.nodeIcon") - .data(nodeData) - .attr("width", Node.DATA_COMPONENT_HEIGHT+4) - .attr("height", Node.DATA_COMPONENT_HEIGHT+4) - .attr("x", function(node:Node){return getIconLocationX(node);}) - .attr("y", function(node:Node){return getIconLocationY(node);}) - .style("display", getIconDisplay) - .enter() - .select("xhtml:div") - .attr("style", function(node:Node){if (eagle.objectIsSelected(node) && node.isCollapsed() && !node.isPeek()){return "background-color:lightgrey; border-radius:4px; border:2px solid "+Config.SELECTED_NODE_COLOR+"; padding:2px; transform:scale(.9);line-height: normal;"}else{return "line-height: normal;padding:4px;transform:scale(.9);"}}) - .enter() - .select("xhtml:span") - .attr("style", function(node:Node){ return node.getGraphIconAttr()}) - .attr("class", function(node:Node){return node.getIcon();}) - - rootContainer - .selectAll("g.node rect.resize-control") - .attr("width", RESIZE_CONTROL_SIZE) - .attr("height", RESIZE_CONTROL_SIZE) - .attr("x", function(node : Node){return getWidth(node) - RESIZE_CONTROL_SIZE;}) - .attr("y", function(node : Node){return getHeight(node) - RESIZE_CONTROL_SIZE;}) - .style("display", getResizeControlDisplay); - - rootContainer - .selectAll("g.node text.resize-control-label") - .attr('x', function(node : Node){return getWidth(node) - RESIZE_CONTROL_SIZE;}) - .attr('y', function(node : Node){return getHeight(node) - 2;}) - .style('font-size', RESIZE_BUTTON_LABEL_FONT_SIZE + 'px') - .style('display', getResizeControlDisplay); - - rootContainer - .selectAll("g.node rect.shrink-button") - .attr("width", SHRINK_BUTTON_SIZE) - .attr("height", SHRINK_BUTTON_SIZE) - .attr("x", function(node : Node){return getWidth(node) - SHRINK_BUTTON_SIZE - HEADER_INSET - 4;}) - .attr("y", HEADER_INSET + 4) - .style("display", getShrinkControlDisplay); - - rootContainer - .selectAll("text.shrink-button-label") - .attr('x', function(node : Node){return getWidth(node) - SHRINK_BUTTON_SIZE - HEADER_INSET - 2;}) - .attr('y', HEADER_INSET + 8 + (SHRINK_BUTTON_SIZE/2)) - .style('font-size', HEADER_BUTTON_LABEL_FONT_SIZE + 'px') - .style('display', getShrinkControlDisplay); - - // inputPorts - nodes - .selectAll("g.inputPorts") - .attr("transform", getInputPortGroupTransform) - .style("display", getPortsDisplay); - - nodes - .selectAll("g.inputPorts text") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .enter() - .select("g.inputPorts") - .insert("text"); - - nodes - .selectAll("g.inputPorts text") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.inputPorts text") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .attr("class", getPortClass) - .attr("x", getInputPortPositionX) - .attr("y", getInputPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - nodes - .selectAll("g.inputPorts circle") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .enter() - .select("g.inputPorts") - .insert("circle"); - - nodes - .selectAll("g.inputPorts circle") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.inputPorts circle") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("cx", getInputPortCirclePositionX) - .attr("cy", getInputPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - nodes - .selectAll("g.inputPorts path") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .enter() - .select("g.inputPorts") - .insert("path"); - - nodes - .selectAll("g.inputPorts path") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.inputPorts path") - .data(function(node : Node){return node.hasInputApplication() ? node.getInputApplicationInputPorts() : node.getInputPorts();}) - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("style", getInputPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // inputLocalPorts - nodes - .selectAll("g.inputLocalPorts") - .attr("transform", getInputLocalPortGroupTransform) - .style("display", getPortsDisplay); - - nodes - .selectAll("g.inputLocalPorts text") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .enter() - .select("g.inputLocalPorts") - .insert("text"); - - nodes - .selectAll("g.inputLocalPorts text") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.inputLocalPorts text") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "event" : ""}) - .attr("x", getInputLocalPortPositionX) - .attr("y", getInputLocalPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - nodes - .selectAll("g.inputLocalPorts circle") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .enter() - .select("g.inputLocalPorts") - .insert("circle"); - - nodes - .selectAll("g.inputLocalPorts circle") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.inputLocalPorts circle") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("cx", getInputLocalPortCirclePositionX) - .attr("cy", getInputLocalPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - nodes - .selectAll("g.inputLocalPorts path") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .enter() - .select("g.inputLocalPorts") - .insert("circle"); - - nodes - .selectAll("g.inputLocalPorts path") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.inputLocalPorts path") - .data(function(node : Node){return node.getInputApplicationOutputPorts();}) - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("style", getInputLocalPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // outputPorts - nodes - .selectAll("g.outputPorts") - .attr("transform", getOutputPortGroupTransform) - .style("display", getPortsDisplay); - - nodes - .selectAll("g.outputPorts text") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .enter() - .select("g.outputPorts") - .insert("text"); - - nodes - .selectAll("g.outputPorts text") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.outputPorts text") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .attr("class", getPortClass) - .attr("x", getOutputPortPositionX) - .attr("y", getOutputPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText()}); - - nodes - .selectAll("g.outputPorts circle") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .enter() - .select("g.outputPorts") - .insert("circle"); - - nodes - .selectAll("g.outputPorts circle") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.outputPorts circle") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("cx", getOutputPortCirclePositionX) - .attr("cy", getOutputPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - nodes - .selectAll("g.outputPorts path") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .enter() - .select("g.outputPorts") - .insert("circle"); - - nodes - .selectAll("g.outputPorts path") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.outputPorts path") - .data(function(node : Node){return node.hasOutputApplication() ? node.getOutputApplicationOutputPorts() : node.getOutputPorts();}) - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("style", getOutputPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "output") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // outputLocalPorts - nodes - .selectAll("g.outputLocalPorts") - .attr("transform", getOutputLocalPortGroupTransform) - .style("display", getPortsDisplay); - - nodes - .selectAll("g.outputLocalPorts text") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .enter() - .select("g.outputLocalPorts") - .insert("text"); - - nodes - .selectAll("g.outputLocalPorts text") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.outputLocalPorts text") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .attr("class", function(port : Field){return port.getIsEvent() ? "event" : ""}) - .attr("x", getOutputLocalPortPositionX) - .attr("y", getOutputLocalPortPositionY) - .style("font-size", PORT_LABEL_FONT_SIZE + "px") - .text(function (port : Field) {return port.getDisplayText();}); - - nodes - .selectAll("g.outputLocalPorts circle") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .enter() - .select("g.outputLocalPorts") - .insert("circle"); - - nodes - .selectAll("g.outputLocalPorts circle") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.outputLocalPorts circle") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("cx", getOutputLocalPortCirclePositionX) - .attr("cy", getOutputLocalPortCirclePositionY) - .attr("r", 6) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - - nodes - .selectAll("g.outputLocalPorts path") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .enter() - .select("g.outputLocalPorts") - .insert("circle"); - - nodes - .selectAll("g.outputLocalPorts path") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .exit() - .remove(); - - nodes - .selectAll("g.outputLocalPorts path") - .data(function(node : Node){return node.getOutputApplicationInputPorts();}) - .attr("d", customTriangle) - .attr("data-id", function(port : Field){return port.getId();}) - .attr("style", getOutputLocalPortTranslatePosition) - .attr("data-node-key", function(port : Field){return port.getNodeKey();}) - .attr("data-usage", "input") - .on("mouseenter", mouseEnterPort) - .on("mouseleave", mouseLeavePort); - - // update attributes of all links - linkExtras - .attr("class", "linkExtra") - .attr("d", createLink) - .attr("fill", "none") - .attr("stroke", "transparent") - .attr("style","pointer-events:visible-stroke;") - .attr("stroke-width", "10px") - .style("display", getEdgeDisplay); - - // update attributes of all links - links - .attr("class", "link") - .attr("d", createLink) - .attr("stroke", edgeGetStrokeColor) - .attr("stroke-dasharray", edgeGetStrokeDashArray) - .attr("fill", "none") - .attr("marker-end", edgeGetArrowheadUrl) - .style("display", getEdgeDisplay); - - // update attributes of all comment links - commentLinks - .attr("class", "commentLink") - .attr("d", createCommentLink) - .attr("stroke", LINK_COLORS.DEFAULT) - .attr("fill", "none") - .attr("marker-end", "ur(#DEFAULT)") - .style("display", getCommentLinkDisplay); - - // dragging link - let draggingX1 : number; - let draggingY1 : number; - let draggingX2 : number; - let draggingY2 : number; - - if (isDraggingPort){ - const srcPortPos = findNodePortPosition(sourceNode, sourcePort.getId(), sourcePortIsInput, false); - - draggingX1 = srcPortPos.x; - draggingY1 = srcPortPos.y; - draggingX2 = DISPLAY_TO_REAL_POSITION_X(mousePosition.x); - draggingY2 = DISPLAY_TO_REAL_POSITION_Y(mousePosition.y); - - // offset x2/y2 so that the draggingLink is not right underneath the cursor (interfering with mouseenter/mouseleave events) - if (draggingX1 > draggingX2) - draggingX2 += 4; - else - draggingX2 -= 4; - if (draggingY1 > draggingY2) - draggingY2 += 4; - else - draggingY2 -= 4; - - // TODO: this is kind of hacky, creating a single-use edge just so that we can determine it's starting position - draggingLink.attr("x1", draggingX1) - .attr("y1", draggingY1) - .attr("x2", draggingX2) - .attr("y2", draggingY2) - .attr("stroke", draggingEdgeGetStrokeColor); - } else { - draggingLink.attr("x1", 0) - .attr("y1", 0) - .attr("x2", 0) - .attr("y2", 0) - .attr("stroke", "none"); - } - - // autocomplete link - if (isDraggingPort && suggestedNode !== null){ - const destPortPos = findNodePortPosition(suggestedNode, suggestedPort.getId(), !sourcePortIsInput, false); - const x2 : number = destPortPos.x; - const y2 : number = destPortPos.y; - - autoCompleteLink.attr("x1", draggingX2) - .attr("y1", draggingY2) - .attr("x2", x2) - .attr("y2", y2) - .attr("stroke", LINK_COLORS.AUTO_COMPLETE); - } else { - autoCompleteLink.attr("x1", 0) - .attr("y1", 0) - .attr("x2", 0) - .attr("y2", 0) - .attr("stroke", "none"); - } - - // selection region - // make sure to send the lesser of the two coordinates as the top left point - selectionRegion - .attr("width", Math.abs(selectionRegionEnd.x - selectionRegionStart.x)) - .attr("height", Math.abs(selectionRegionEnd.y - selectionRegionStart.y)) - .attr("x", selectionRegionStart.x <= selectionRegionEnd.x ? selectionRegionStart.x : selectionRegionEnd.x) - .attr("y", selectionRegionStart.y <= selectionRegionEnd.y ? selectionRegionStart.y : selectionRegionEnd.y) - .attr("stroke", "black") - .attr("fill", "transparent") - .style("display", "inline"); - - const elapsedTime = performance.now() - startTime; - if (elapsedTime > eagle.rendererFrameMax){eagle.rendererFrameMax = elapsedTime;} - eagle.rendererFrameDisplay("tick " + elapsedTime.toFixed(2) + "ms (max " + eagle.rendererFrameMax.toFixed(2) + "ms) Renders " + eagle.rendererFrameCountRender + " Ticks " + eagle.rendererFrameCountTick); - } - - function selectEdge(edge : Edge, addToSelection: boolean){ - if (edge !== null){ - if (addToSelection){ - eagle.editSelection(Eagle.RightWindowMode.Inspector, edge, Eagle.FileType.Graph); - } else { - eagle.setSelection(Eagle.RightWindowMode.Inspector, edge, Eagle.FileType.Graph); - } - } - } - - function selectNode(node : Node, addToSelection: boolean){ - if (node !== null){ - if (addToSelection){ - eagle.editSelection(Eagle.RightWindowMode.Inspector, node, Eagle.FileType.Graph); - } else { - eagle.setSelection(Eagle.RightWindowMode.Inspector, node, Eagle.FileType.Graph); - } - } - } - - function buildTranslation(x : number, y : number) : string { - return "translate(" + x.toString() + "," + y.toString() + ")"; - } - - function getContentText(data : Node) : string { - return data.getCustomData(); - } - - function rootScaleTranslation(data : Node, e : any) : string { - //console.log("rootScaleTranslation()", eagle.globalOffsetX, eagle.globalOffsetY, eagle.globalScale); - return "translate(" + eagle.globalOffsetX + "," + eagle.globalOffsetY + ")scale(" + eagle.globalScale + ")"; - } - - function nodeGetTranslation(data : Node) : string { - //return buildTranslation(REAL_TO_DISPLAY_POSITION_X(data.getPosition().x), REAL_TO_DISPLAY_POSITION_Y(data.getPosition().y)); - return buildTranslation(data.getPosition().x, data.getPosition().y); - } - - function getX(node : Node) : number { - return node.getPosition().x; - } - - function getY(node : Node) : number { - return node.getPosition().y; - } - - function getWidth(node : Node) : number { - return node.getDisplayWidth(); - } - - function getHeight(node : Node) : number { - return node.getDisplayHeight(); - } - - function getIconLocationX(node : Node) : number { - return node.getWidth()/2 - Node.DATA_COMPONENT_WIDTH/2; - } - - function getIconLocationY(node : Node) : number { - return Node.DATA_COMPONENT_HEIGHT/4; - } - - function getHeaderBackgroundDisplay(node : Node) : string { - // don't show header background for comment, description and ExclusiveForceNode nodes - if (node.getCategory() === Category.Comment || - node.getCategory() === Category.Description || - node.getCategory() === Category.ExclusiveForceNode || - node.getCategory() === Category.Branch) { - return "none"; - } - - return !node.isGroup() && node.isCollapsed() && !node.isPeek() ? "none" : "inline"; - } - - function getHeaderBackgroundWidth(node : Node) : number { - return getWidth(node) - HEADER_INSET*2; - } - - function getHeaderBackgroundHeight(node : Node) : number { - if (node.isGroup() && node.isCollapsed()){ - return Node.GROUP_COLLAPSED_HEIGHT - HEADER_INSET*2; - } - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return Node.DATA_COMPONENT_HEIGHT; - } - - // default height - return 8 + (20 * node.getNameNumLines(node.getDisplayWidth())); - } - - function getHeaderDisplay(node : Node) : string { - // don't show header background for comment and description nodes - if (node.getCategory() === Category.Comment || node.getCategory() === Category.Description){ - return "none"; - } else { - return "inline"; - } - } - - function getHeaderText(data : Node) : string { - return data.getName(); - } - - function getHeaderPositionX(node : Node) : number { - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return node.getWidth()/2; - } - - return getWidth(node) /2; - } - - function getHeaderPositionY(node : Node) : number { - if (node.isGroup() && node.isCollapsed()){ - - // decide how many lines this will be and move upwards some amount - if (node.getNameNumLines(node.getDisplayWidth()) > 1){ - return Node.GROUP_COLLAPSED_HEIGHT / 3; - } - - return Node.GROUP_COLLAPSED_HEIGHT / 2; - } - - if (!node.isCollapsed() || node.isPeek()){ - return CategoryData.getCategoryData(node.getCategory()).expandedHeaderOffsetY; - } else { - return CategoryData.getCategoryData(node.getCategory()).collapsedHeaderOffsetY; - } - } - - function getHeaderFill(node : Node) : string { - if (eagle.objectIsSelected(node) && node.isCollapsed() && !node.isPeek()){ - return Config.SELECTED_NODE_COLOR; - } - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return "black"; - } - - if (node.getCategory() === Category.ExclusiveForceNode){ - return "black"; - } - - return "white"; - } - - function getHeaderFontWeight(node : Node) : string { - if (eagle.objectIsSelected(node)){ - return "bold"; - } - - return "normal"; - } - - function getAppsBackgroundDisplay(node : Node) : string { - // if node is collapsed, return 'none' - if (node.isCollapsed()){ - return "none"; - } - - // if a service is not showing ports, hide - if (node.isService() && node.isCollapsed() && !node.isPeek()){ - return "none"; - } - - // if node has input or output apps, return 'inline' else 'none' - if (Node.canHaveInputApp(node) || Node.canHaveOutputApp(node) ){ - return "inline"; - } - - return "none"; - } - - function getAppsBackgroundWidth(node : Node) : number { - return getWidth(node) - HEADER_INSET*2; - } - - function getAppsBackgroundHeight(node : Node) : number { - if (node.isGroup() && node.isCollapsed()){ - return Node.GROUP_COLLAPSED_HEIGHT; - } - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return Node.DATA_COMPONENT_HEIGHT; - } - - // default height - return APPS_HEIGHT; - } - - function getInputAppText(node:Node) : string { - if (!Node.canHaveInputApp(node)){ - return ""; - } - - const inputApplication : Node = node.getInputApplication(); - - if (typeof inputApplication === "undefined" || inputApplication === null){ - return Node.NO_APP_STRING; - } - - return inputApplication.getName(); - } - - function getInputAppPositionX(node : Node) : number { - return 8; - } - - function getInputAppPositionY(node : Node) : number { - return getHeaderBackgroundHeight(node) + 20; - } - - function getOutputAppText(node:Node) : string { - if (!Node.canHaveOutputApp(node)){ - return ""; - } - - const outputApplication : Node = node.getOutputApplication(); - - if (typeof outputApplication === "undefined" || outputApplication === null){ - return Node.NO_APP_STRING; - } - - return outputApplication.getName(); - } - - function getOutputAppPositionX(node : Node) : number { - return node.getWidth() - 8; - } - - function getOutputAppPositionY(node : Node) : number { - return getHeaderBackgroundHeight(node) + 20; - } - - function getInputPortGroupClass(node : Node) : string { - if (node.isFlipPorts()){ - return "inputPorts flipped"; - } else { - return "inputPorts no-flip"; - } - } - - function getOutputPortGroupClass(node : Node) : string { - if (node.isFlipPorts()){ - return "outputPorts flipped"; - } else { - return "outputPorts no-flip"; - } - } - - function getInputLocalPortGroupClass(node : Node) : string { - if (node.isFlipPorts()){ - return "inputLocalPorts flipped"; - } else { - return "inputLocalPorts no-flip"; - } - } - - function getOutputLocalPortGroupClass(node : Node) : string { - if (node.isFlipPorts()){ - return "outputLocalPorts flipped"; - } else { - return "outputLocalPorts no-flip"; - } - } - - function getPortClass(port : Field, index: number): string { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return ""; - } - - if (node.isBranch()){ - if (index === 0){ - return port.getIsEvent() ? "event middle" : "middle"; - } - if (index === 1){ - return port.getIsEvent() ? "event" : ""; - } - } - - return port.getIsEvent() ? "event" : ""; - } - - function getInputPortGroupTransform(node : Node) : string { - if (node.isBranch()){ - return buildTranslation(0, 0); - } - - if (node.isFlipPorts()){ - return getRightSidePortGroupTransform(node); - } else { - return getLeftSidePortGroupTransform(node); - } - } - - function getOutputPortGroupTransform(node : Node) : string { - if (node.isBranch()){ - return buildTranslation(0, 0); - } - - if (node.isFlipPorts()){ - return getLeftSidePortGroupTransform(node); - } else { - return getRightSidePortGroupTransform(node); - } - } - - function getInputLocalPortGroupTransform(node : Node) : string { - if (node.isFlipPorts()){ - return getRightSideLocalPortGroupTransform(node); - } else { - return getLeftSideLocalPortGroupTransform(node); - } - } - - function getOutputLocalPortGroupTransform(node : Node) : string { - if (node.isFlipPorts()){ - return getLeftSideLocalPortGroupTransform(node); - } else { - return getRightSideLocalPortGroupTransform(node); - } - } - - function getLeftSidePortGroupTransform(node : Node) : string { - if (Node.canHaveInputApp(node) || Node.canHaveOutputApp(node)){ - return buildTranslation(PORT_OFFSET_X, getHeaderBackgroundHeight(node) + APPS_HEIGHT); - } else { - return buildTranslation(PORT_OFFSET_X, getHeaderBackgroundHeight(node)); - } - } - - function getRightSidePortGroupTransform(node : Node) : string { - if (Node.canHaveInputApp(node) || Node.canHaveOutputApp(node)){ - return buildTranslation(getWidth(node)-PORT_OFFSET_X, getHeaderBackgroundHeight(node) + APPS_HEIGHT); - } else { - return buildTranslation(getWidth(node)-PORT_OFFSET_X, getHeaderBackgroundHeight(node)); - } - } - - function getLeftSideLocalPortGroupTransform(node : Node) : string { - if (Node.canHaveInputApp(node) || Node.canHaveOutputApp(node)){ - return buildTranslation(PORT_OFFSET_X, getHeaderBackgroundHeight(node) + APPS_HEIGHT + node.getInputApplicationInputPorts().length * PORT_HEIGHT); - } else { - return buildTranslation(PORT_OFFSET_X, getHeaderBackgroundHeight(node) + node.getInputPorts().length * PORT_HEIGHT); - } - } - - function getRightSideLocalPortGroupTransform(node : Node) : string { - if (Node.canHaveInputApp(node) || Node.canHaveOutputApp(node)){ - return buildTranslation(getWidth(node)-PORT_OFFSET_X, getHeaderBackgroundHeight(node) + APPS_HEIGHT + (node.getOutputApplicationOutputPorts().length) * PORT_HEIGHT); - } else { - return buildTranslation(getWidth(node)-PORT_OFFSET_X, getHeaderBackgroundHeight(node) + node.getOutputPorts().length * PORT_HEIGHT); - } - } - - // TODO: one level of indirection here (getInput/Output -> getLeft/Right -> position) - function getInputPortPositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getLeftSidePortPositionX(port, index); - } - - if (node.isBranch()){ - const numPorts = node.getInputPorts().length; - return 100 - 76 * portIndexRatio(index, numPorts); - } - - if (node.isFlipPorts()){ - return getRightSidePortPositionX(port, index); - } else { - return getLeftSidePortPositionX(port, index); - } - } - - function getInputPortPositionY(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getPortPositionY(port, index); - } - - if (node.isBranch()){ - const numPorts = node.getInputPorts().length; - return 24 + 30 * portIndexRatio(index, numPorts); - } - - return getPortPositionY(port, index); - } - - function getOutputPortPositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortPositionX(port, index); - } - - if (node.isBranch()){ - if (index === 0){ - return 200 / 2; - } - if (index === 1){ - return 200 - 24; - } - } - - if (node.isFlipPorts()){ - return getLeftSidePortPositionX(port, index); - } else { - return getRightSidePortPositionX(port, index); - } - } - - function getOutputPortPositionY(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getPortPositionY(port, index); - } - - if (node.isBranch()){ - if (index === 0){ - return 100 - 16; - } - if (index === 1){ - return 54; - } - } - - return getPortPositionY(port, index); - } - - function getInputLocalPortPositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getLeftSidePortPositionX(port, index); - } - - if (node.isBranch()){ - if (index === 0){ - return 200 / 2; - } - if (index === 1){ - return 200 - 24; - } - } - - if (node.isFlipPorts()){ - return getRightSidePortPositionX(port, index); - } else { - return getLeftSidePortPositionX(port, index); - } - } - - function getInputLocalPortPositionY(port : Field, index : number) : number { - return getPortPositionY(port, index); - } - - function getOutputLocalPortPositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortPositionX(port, index); - } - - if (node.isFlipPorts()){ - return getLeftSidePortPositionX(port, index); - } else { - return getRightSidePortPositionX(port, index); - } - } - - function getOutputLocalPortPositionY(port : Field, index : number) : number { - return getPortPositionY(port, index); - } - - function getExitLocalPortPositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortPositionX(port, index); - } - - if (node.isFlipPorts()){ - return getLeftSidePortPositionX(port, index); - } else { - return getRightSidePortPositionX(port, index); - } - } - - function getExitLocalPortPositionY(port : Field, index : number) : number { - return getPortPositionY(port, index); - } - - function getLeftSidePortPositionX(port : Field, index : number) : number { - return 20; - } - - function getPortPositionY(port : Field, index : number) : number { - return (index + 1) * PORT_HEIGHT; - } - - function getRightSidePortPositionX(port : Field, index : number) : number { - return -20; - } - - function getInputPortTranslatePosition(port:Field, index:number) : string { - const posX = getInputPortCirclePositionX(port, index) - const posY = getInputPortCirclePositionY(port,index) - - return "transform: translate("+posX+"px,"+posY+"px) rotate(90deg)" - } - - function getOutputPortTranslatePosition(port:Field, index:number) : string { - const posX = getOutputPortCirclePositionX(port, index) - const posY = getOutputPortCirclePositionY(port,index) - - return "transform:translate("+posX+"px,"+posY+"px) rotate(270deg)" - } - - function getInputLocalPortTranslatePosition(port:Field, index:number) : string { - const posX = getInputLocalPortCirclePositionX(port, index) - const posY = getInputLocalPortCirclePositionY(port,index) - - return "transform: translate("+posX+"px,"+posY+"px) rotate(90deg)" - } - - function getOutputLocalPortTranslatePosition(port:Field, index:number) : string { - const posX = getOutputLocalPortCirclePositionX(port, index) - const posY = getOutputLocalPortCirclePositionY(port,index) - - return "transform:translate("+posX+"px,"+posY+"px) rotate(270deg)" - } - - // port circle positions - function getInputPortCirclePositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getLeftSidePortCirclePositionX(port, index); - } - - if (node.isBranch()){ - const numPorts = node.getInputPorts().length; - return 100 - 100 * portIndexRatio(index, numPorts); - } - - if (node.isFlipPorts()){ - return getRightSidePortCirclePositionX(port, index); - } else { - return getLeftSidePortCirclePositionX(port, index); - } - } - function getInputPortCirclePositionY(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getPortCirclePositionY(port, index); - } - - if (node.isBranch()){ - const numPorts = node.getInputPorts().length; - return 50 * portIndexRatio(index, numPorts); - } - - return getPortCirclePositionY(port, index); - } - function getOutputPortCirclePositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortCirclePositionX(port, index); - } - - if (node.isBranch()){ - if (index === 0){ - return 200 / 2; - } - if (index === 1){ - return 200; - } - } - - if (node.isFlipPorts()){ - return getLeftSidePortCirclePositionX(port, index); - } else { - return getRightSidePortCirclePositionX(port, index); - } - } - function getOutputPortCirclePositionY(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getPortCirclePositionY(port, index); - } - - if (node.isBranch()){ - // TODO: magic number - if (index === 0){ - return 100; - } - if (index === 1){ - return 100 / 2; - } - } - - return getPortCirclePositionY(port, index); - } - function getExitPortCirclePositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortCirclePositionX(port, index); - } - - if (node.isFlipPorts()){ - return getLeftSidePortCirclePositionX(port, index); - } else { - return getRightSidePortCirclePositionX(port, index); - } - } - function getExitPortCirclePositionY(port : Field, index : number) : number { - return getPortCirclePositionY(port, index); - } - function getInputLocalPortCirclePositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getLeftSidePortCirclePositionX(port, index); - } - - if (node.isFlipPorts()){ - return getRightSidePortCirclePositionX(port, index); - } else { - return getLeftSidePortCirclePositionX(port, index); - } - } - function getInputLocalPortCirclePositionY(port : Field, index : number) : number { - return getPortCirclePositionY(port, index); - } - function getOutputLocalPortCirclePositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortCirclePositionX(port, index); - } - - if (node.isFlipPorts()){ - return getLeftSidePortCirclePositionX(port, index); - } else { - return getRightSidePortCirclePositionX(port, index); - } - } - function getOutputLocalPortCirclePositionY(port : Field, index : number) : number { - return getPortCirclePositionY(port, index); - } - function getExitLocalPortCirclePositionX(port : Field, index : number) : number { - const node: Node = findNodeWithKey(port.getNodeKey(), nodeData); - - if (node === null){ - console.warn("Unable to find node from port's node key", port.getNodeKey()); - return getRightSidePortCirclePositionX(port, index); - } - - if (node.isFlipPorts()){ - return getLeftSidePortCirclePositionX(port, index); - } else { - return getRightSidePortCirclePositionX(port, index); - } - } - function getExitLocalPortCirclePositionY(port : Field, index : number) : number { - return getPortCirclePositionY(port, index); - } - - function getLeftSidePortCirclePositionX(port : Field, index : number) : number { - return 8; - } - - function getPortCirclePositionY(port : Field, index : number) : number { - return (index + 1) * PORT_HEIGHT - 5; - } - - function getRightSidePortCirclePositionX(port : Field, index : number) : number { - return -8; - } - - function getContentPositionX(node : Node) : number { - // left justified - return 8; - } - - function getContentPositionY(node : Node) : number { - // top - return 16; - } - - function getContentFill() : string { - return "black"; - } - - function getContentDisplay(node : Node) : string { - // only show content for comment and description nodes - if ((node.getCategory() === Category.Comment || node.getCategory() === Category.Description) && (!node.isCollapsed() || node.isPeek())){ - return "inline"; - } else { - return "none"; - } - } - - function getIconDisplay(node : Node) : string { - if (!node.isGroup() && !(!node.isCollapsed() || node.isPeek()) && !node.isBranch()){ - return "inline" - } else { - return "none"; - } - } - - function nodeGetColor(node : Node) : string { - return node.getColor(); - } - - function nodeGetFill(node : Node) : string { - //console.log("nodeGetFill() category", node.getCategory()); - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return "none"; - } - - // no fill color for "ExclusiveForceNode" nodes - if (node.getCategory() === Category.ExclusiveForceNode){ - return "white"; - } - - return "rgba(180,180,180,1)"; - } - - function nodeGetStroke(node : Node) : string { - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return "none"; - } - - if (eagle.objectIsSelected(node)){ - return "black"; - } - - return "grey"; - } - - function nodeGetStrokeDashArray(node: Node) : string { - if (node.getCategory() === Category.ExclusiveForceNode){ - return "8"; - } - return ""; - } - - function findDepthOfNode(index: number, nodes : Node[]) : number { - if (index >= nodes.length){ - console.warn("findDepthOfNode() with node index outside range of nodes. index:", index, "nodes.length", nodes.length); - return 0; - } - - let depth : number = 0; - let node : Node = nodes[index]; - let nodeKey : number; - let nodeParentKey : number = node.getParentKey(); - let iterations = 0; - - // follow the chain of parents - while (nodeParentKey != null){ - if (iterations > 10){ - console.error("too many iterations in findDepthOfNode()"); - break; - } - - iterations += 1; - depth += 1; - depth += node.getDrawOrderHint() / 10; - nodeKey = node.getKey(); - nodeParentKey = node.getParentKey(); - - if (nodeParentKey === null){ - return depth; - } - - node = findNodeWithKey(nodeParentKey, nodes); - - if (node === null){ - console.error("Node", nodeKey, "has parentKey", nodeParentKey, "but call to findNodeWithKey(", nodeParentKey, ") returned null"); - return depth; - } - - // if parent is selected, add more depth, so that it will appear on top - if (eagle.objectIsSelected(node)){ - depth += 10; - } - } - - depth += node.getDrawOrderHint() / 10; - - // if node is selected, add more depth, so that it will appear on top - if (eagle.objectIsSelected(node)){ - depth += 10; - } - - return depth; - } - - function depthFirstTraversalOfNodes(graph: LogicalGraph, showDataNodes: boolean) : Node[] { - const indexPlusDepths : {index:number, depth:number}[] = []; - const result : Node[] = []; - - // populate key plus depths - for (let i = 0 ; i < graph.getNodes().length ; i++){ - let nodeHasConnectedInput: boolean = false; - let nodeHasConnectedOutput: boolean = false; - const node = graph.getNodes()[i]; - - // check if node has connected input and output - for (const edge of graph.getEdges()){ - if (edge.getDestNodeKey() === node.getKey()){ - nodeHasConnectedInput = true; - } - - if (edge.getSrcNodeKey() === node.getKey()){ - nodeHasConnectedOutput = true; - } - } - - // skip data nodes, if showDataNodes is false - if (!showDataNodes && node.isData() && nodeHasConnectedInput && nodeHasConnectedOutput){ - continue; - } - - const depth = findDepthOfNode(i, graph.getNodes()); - - indexPlusDepths.push({index:i, depth:depth}); - } - - // sort nodes in depth ascending - indexPlusDepths.sort(function(a, b){ - return a.depth - b.depth; - }); - - // write nodes to result in sorted order - for (const indexPlusDepth of indexPlusDepths){ - result.push(graph.getNodes()[indexPlusDepth.index]); - } - - return result; - } - - function findNodeWithKey(key: number, nodes: Node[]) : Node { - if (key === null){ - return null; - } - - for (const node of nodes){ - if (node.getKey() === key){ - return node; - } - - // check if the node's inputApp has a matching key - if (node.hasInputApplication()){ - if (node.getInputApplication().getKey() === key){ - return node.getInputApplication(); - } - } - - // check if the node's outputApp has a matching key - if (node.hasOutputApplication()){ - if (node.getOutputApplication().getKey() === key){ - return node.getOutputApplication(); - } - } - } - - console.warn("Cannot find node with key", key); - return null; - } - - function getEdgeDisplay(edge : Edge) : string { - const srcNode : Node = findNodeWithKey(edge.getSrcNodeKey(), nodeData); - const destNode : Node = findNodeWithKey(edge.getDestNodeKey(), nodeData); - - if (srcNode === null || destNode === null){ - return "none"; - } - - if (findAncestorCollapsedNode(srcNode) !== null && findAncestorCollapsedNode(destNode) !== null){ - return "none"; - } - - // also collapse if source port is local port of collapsed node - if (srcNode.hasLocalPortWithId(edge.getSrcPortId()) && srcNode.isCollapsed()){ - return "none"; - } - - return "inline"; - } - - function branchPortPosition(node: Node, portId: string, input: boolean) : {x: number, y: number}{ - const portIndex = findNodePortIndex(node, portId); - const sourceIsInput = node.findPortIsInputById(portId); - - if (node.isCollapsed()){ // TODO: maybe add && !node.isPeek() here - if (input){ - if (portIndex === 0){ - return { - x: node.getPosition().x + node.getWidth()/2, - y: node.getPosition().y - }; - } else { - return { - x: node.getPosition().x + node.getWidth()*1/4, - y: node.getPosition().y + node.getHeight()*3/4 - 4 - }; - } - } else { - if (portIndex === 0){ - return { - x: node.getPosition().x + node.getWidth()/2, - y: node.getPosition().y + node.getHeight() - }; - } else { - return { - x: node.getPosition().x + node.getWidth()*3/4, - y: node.getPosition().y + node.getHeight()*3/4 - 4 - }; - } - } - } - else { // not collapsed - if (input){ - // calculate position of edge starting from a branch INPUT port - if (portIndex === 0){ - return { - x: node.getPosition().x + node.getWidth()/2, - y: node.getPosition().y - }; - } else { - return { - x: node.getPosition().x, - y: node.getPosition().y + 50 - }; - } - } else { - if (portIndex === 0){ - return { - x: node.getPosition().x + node.getWidth()/2, - y: node.getPosition().y + 100 - }; - } else { - return { - x: node.getPosition().x + node.getWidth(), - y: node.getPosition().y + 50 - }; - } - } - } - - return {x:0, y:0}; - } - - function portIndexRatio(portIndex: number, numPorts: number){ - if (numPorts <= 1){ - return 0; - } - - return portIndex / (numPorts - 1); - } - - function findNodePortPosition(node : Node, portId: string, input: boolean, inset: boolean) : {x: number, y: number} { - let local : boolean; - let index : number; - const flipped : boolean = node.isFlipPorts(); - const position = {x: node.getPosition().x, y: node.getPosition().y}; - - //console.log("findNodePortPosition()", "portId", portId, "input", input, "inset", inset, "node.isBranch()", node.isBranch(), "node.isEmbedded()", node.isEmbedded()); - - // check if an ancestor is collapsed, if so, use center of ancestor - const collapsedAncestor : Node = findAncestorCollapsedNode(node); - if (collapsedAncestor !== null){ - return { - x: collapsedAncestor.getPosition().x + Node.GROUP_COLLAPSED_WIDTH, - y: collapsedAncestor.getPosition().y - }; - } - - // check if node is a branch - if (node.isBranch()){ - return branchPortPosition(node, portId, input); - } - - // check if node is an embedded app, if so, use position of the construct in which the app is embedded - if (node.isEmbedded()){ - const containingConstruct : Node = findNodeWithKey(node.getEmbedKey(), nodeData); - return findNodePortPosition(containingConstruct, portId, input, true); - } - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - if ((input && !node.isFlipPorts()) || (!input && node.isFlipPorts())){ - return { - x: node.getPosition().x + getIconLocationX(node), - y: node.getPosition().y + getIconLocationY(node) + Node.DATA_COMPONENT_HEIGHT/2 - }; - } else { - return { - x: node.getPosition().x + getIconLocationX(node) + Node.DATA_COMPONENT_WIDTH, - y: node.getPosition().y + getIconLocationY(node) + Node.DATA_COMPONENT_HEIGHT/2 - }; - } - } - - // find the port within the node - if (input){ - for (let i = 0 ; i < node.getInputPorts().length ; i++){ - const port : Field = node.getInputPorts()[i]; - if (port.getId() === portId){ - local = false; - index = i; - } - } - } - - if (!input){ - for (let i = 0 ; i < node.getOutputPorts().length ; i++){ - const port : Field = node.getOutputPorts()[i]; - if (port.getId() === portId){ - local = false; - index = i; - } - } - } - - // check input application ports - if (input){ - for (let i = 0 ; i < node.getInputApplicationInputPorts().length ; i++){ - const port : Field = node.getInputApplicationInputPorts()[i]; - if (port.getId() === portId){ - local = false; - index = i; - } - } - } - - if (!input){ - for (let i = 0 ; i < node.getInputApplicationOutputPorts().length ; i++){ - const port : Field = node.getInputApplicationOutputPorts()[i]; - if (port.getId() === portId){ - local = true; - index = i + node.getInputApplicationInputPorts().length; - } - } - } - - // check output application ports - if (input){ - for (let i = 0 ; i < node.getOutputApplicationInputPorts().length ; i++){ - const port : Field = node.getOutputApplicationInputPorts()[i]; - if (port.getId() === portId){ - local = true; - index = i + node.getOutputApplicationOutputPorts().length; - } - } - } - - if (!input){ - for (let i = 0 ; i < node.getOutputApplicationOutputPorts().length ; i++){ - const port : Field = node.getOutputApplicationOutputPorts()[i]; - if (port.getId() === portId){ - local = false; - index = i; - } - } - } - - // determine whether we need to move down an extra amount to clear the apps display title row - let appsOffset : number = 0; - if (Node.canHaveInputApp(node) || Node.canHaveOutputApp(node)){ - appsOffset = APPS_HEIGHT; - } - - // translate the three pieces of info into the x,y position - let drawLeftHandSide: boolean = false; - if (input){ - drawLeftHandSide = !drawLeftHandSide; - } - if (flipped){ - drawLeftHandSide = !drawLeftHandSide; - } - if (local){ - drawLeftHandSide = !drawLeftHandSide; - } - //console.log("input", input, "flipped", flipped, "local", local, "index", index, "drawLeftHandSide", drawLeftHandSide); - - const headerHeight: number = getHeaderBackgroundHeight(node); - - // outer if is an XOR - if (drawLeftHandSide){ - // left hand side - if (inset){ - position.x += PORT_INSET; - } - if (local){ - position.y += headerHeight + appsOffset + (node.getInputPorts().length + index + 1) * PORT_HEIGHT; - } else { - position.y += headerHeight + appsOffset + (index + 1) * PORT_HEIGHT; - } - } else { - // right hand side - if (inset){ - position.x += node.getWidth() - PORT_INSET; - } else { - position.x += node.getWidth(); - } - if (local){ - position.y += headerHeight + appsOffset + (node.getOutputPorts().length + index + 1) * PORT_HEIGHT; - } else { - position.y += headerHeight + appsOffset + (index + 1) * PORT_HEIGHT; - } - } - - //console.log("position", position); - - return position; - } - - function findNodePortIndex(node: Node, portId: string){ - // find the port within the node - for (let i = 0 ; i < node.getInputPorts().length ; i++){ - const port : Field = node.getInputPorts()[i]; - if (port.getId() === portId){ - return i; - } - } - - for (let i = 0 ; i < node.getOutputPorts().length ; i++){ - const port : Field = node.getOutputPorts()[i]; - if (port.getId() === portId){ - return i; - } - } - - return -1; - } - - function edgeGetStrokeColor(edge: Edge, index: number) : string { - let normalColor: string = LINK_COLORS.DEFAULT; - let selectedColor: string = LINK_COLORS.DEFAULT_SELECTED; - - // check if source node is an event, if so, draw in blue - const srcNode : Node = eagle.logicalGraph().findNodeByKey(edge.getSrcNodeKey()); - - if (srcNode !== null){ - const srcPort : Field = srcNode.findFieldById(edge.getSrcPortId()); - - if (srcPort !== null && srcPort.getIsEvent()){ - normalColor = LINK_COLORS.EVENT; - selectedColor = LINK_COLORS.EVENT_SELECTED; - } - } - - // check if link has a warning or is invalid - const linkValid : Eagle.LinkValid = Edge.isValid(eagle, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.getDataType(), edge.isLoopAware(), edge.isClosesLoop(), false, false, {errors:[], warnings:[]}); - - if (linkValid === Eagle.LinkValid.Invalid){ - normalColor = LINK_COLORS.INVALID; - selectedColor = LINK_COLORS.INVALID_SELECTED; - } - - if (linkValid === Eagle.LinkValid.Warning){ - normalColor = LINK_COLORS.WARNING; - selectedColor = LINK_COLORS.WARNING_SELECTED; - } - - // check if the edge is a "closes loop" edge - if (edge.isClosesLoop()){ - normalColor = LINK_COLORS.CLOSES_LOOP; - selectedColor = LINK_COLORS.CLOSES_LOOP_SELECTED; - } - - return eagle.objectIsSelected(edge) ? selectedColor : normalColor; - } - - // TODO: this is inefficient, it calls edgeGetStrokeColor that determines the correct color enum and returns the color - // then this function uses the color to get back to the enum - function edgeGetArrowheadUrl(edge: Edge, index: number) { - const selectedEdgeColor = edgeGetStrokeColor(edge, index); - const findResult = Object.entries(LINK_COLORS).find(value => value[1] === selectedEdgeColor); - return "url(#"+findResult[0]+")"; - } - - function edgeGetStrokeDashArray(edge: Edge, index: number) : string { - const srcNode : Node = eagle.logicalGraph().findNodeByKey(edge.getSrcNodeKey()); - const destNode : Node = eagle.logicalGraph().findNodeByKey(edge.getDestNodeKey()); - - // if we can't find the edge - if (srcNode === null){ - return ""; - } - if (destNode === null){ - return ""; - } - - if (srcNode.isStreaming() || destNode.isStreaming()){ - return "8"; - } - - if (edge.isClosesLoop()){ - return "8 8 2 8" - } - - return ""; - } - - function draggingEdgeGetStrokeColor(edge: Edge, index: number) : string { - switch (isDraggingPortValid){ - case Eagle.LinkValid.Unknown: - return "black"; - case Eagle.LinkValid.Invalid: - return LINK_COLORS.INVALID; - case Eagle.LinkValid.Warning: - return LINK_COLORS.WARNING; - case Eagle.LinkValid.Valid: - return LINK_COLORS.VALID; - } - } - - function addEdge(srcNode: Node, srcPort: Field, destNode: Node, destPort: Field, loopAware: boolean, closesLoop: boolean) : void { - if (srcPort.getId() === destPort.getId()){ - console.warn("Abort addLink() from port to itself!"); - return; - } - - eagle.addEdge(srcNode, srcPort, destNode, destPort, loopAware, closesLoop, (edge : Edge) : void => { - eagle.checkGraph(); - eagle.logicalGraph.valueHasMutated(); - clearEdgeVars(); - }); - } - - function clearEdgeVars(){ - sourcePort = null; - sourceNode = null; - sourcePortIsInput = false; - destinationPort = null; - destinationNode = null; - suggestedPort = null; - suggestedNode = null; - } - - function createCommentLink(node : Node){ - // abort if node is not comment - if (node.getCategory() !== Category.Comment){ - return ""; - } - - // abort if comment node has no subject - if (node.getSubjectKey() === null){ - return ""; - } - - // find subject node - const subjectNode : Node = findNodeWithKey(node.getSubjectKey(), nodeData); - - let x1, y1, x2, y2; - - if (node.isFlipPorts()){ - x1 = node.getPosition().x; - y1 = node.getPosition().y; - } else { - x1 = node.getPosition().x + node.getWidth(); - y1 = node.getPosition().y; - } - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - if (node.isFlipPorts()){ - x1 = node.getPosition().x + getIconLocationX(node); - y1 = node.getPosition().y + getIconLocationY(node); - } else { - x1 = node.getPosition().x + getIconLocationX(node) + Node.DATA_COMPONENT_WIDTH; - y1 = node.getPosition().y + getIconLocationY(node) + Node.DATA_COMPONENT_HEIGHT/2; - } - } - - if (subjectNode.isFlipPorts()){ - x2 = subjectNode.getPosition().x + subjectNode.getWidth(); - y2 = subjectNode.getPosition().y; - } else { - x2 = subjectNode.getPosition().x; - y2 = subjectNode.getPosition().y; - } - - if (!subjectNode.isGroup() && subjectNode.isCollapsed() && !subjectNode.isPeek()){ - if (subjectNode.isFlipPorts()){ - x2 = subjectNode.getPosition().x + getIconLocationX(subjectNode) + Node.DATA_COMPONENT_WIDTH; - y2 = subjectNode.getPosition().y + getIconLocationY(subjectNode) + Node.DATA_COMPONENT_HEIGHT/2; - } else { - x2 = subjectNode.getPosition().x + getIconLocationX(subjectNode); - y2 = subjectNode.getPosition().y + getIconLocationY(subjectNode) + Node.DATA_COMPONENT_HEIGHT/2; - } - } - - if (subjectNode.isBranch()){ - x2 = subjectNode.getPosition().x + subjectNode.getWidth()/2; - y2 = subjectNode.getPosition().y; - } - - // determine incident directions for start and end of edge - const startDirection = node.isFlipPorts() ? Eagle.Direction.Left : Eagle.Direction.Right; - let endDirection = subjectNode.isFlipPorts() ? Eagle.Direction.Left : Eagle.Direction.Right; - - if (subjectNode.isBranch()){ - endDirection = Eagle.Direction.Down; - } - - return createBezier(x1, y1, x2, y2, startDirection, endDirection, false); - } - - function getCommentLinkDisplay(node : Node) : string { - if (node.getCategory() !== Category.Comment){ - return "none"; - } - - if (node.getSubjectKey() === null){ - return "none"; - } - - return "inline"; - } - - function directionOffset(x: boolean, direction: Eagle.Direction){ - if (x){ - switch (direction){ - case Eagle.Direction.Left: - return -50; - case Eagle.Direction.Right: - return 50; - default: - return 0; - } - } else { - switch (direction){ - case Eagle.Direction.Up: - return -50; - case Eagle.Direction.Down: - return 50; - default: - return 0; - } - } - } - - function createBezier(x1: number, y1: number, x2: number, y2: number, startDirection: Eagle.Direction, endDirection: Eagle.Direction, isLoop: boolean) : string { - if (isLoop){ - // find control points - const c1x = x1 + 3 * directionOffset(true, startDirection); - const c1y = y1 + 1.5 * directionOffset(true, startDirection); - const c2x = x2 - 3 * directionOffset(true, endDirection); - const c2y = y2 + 1.5 * directionOffset(true, endDirection); - - return "M " + x1 + " " + y1 + " C " + c1x + " " + c1y + ", " + c2x + " " + c2y + ", " + x2 + " " + y2; - } else { - // find control points - const c1x = x1 + directionOffset(true, startDirection); - const c1y = y1 + directionOffset(false, startDirection); - const c2x = x2 - directionOffset(true, endDirection); - const c2y = y2 - directionOffset(false, endDirection); - - return "M " + x1 + " " + y1 + " C " + c1x + " " + c1y + ", " + c2x + " " + c2y + ", " + x2 + " " + y2; - } - } - - function shrinkOnClick(node : Node, index : number){ - console.log("shrink node", index); - - eagle.logicalGraph().shrinkNode(node); - eagle.logicalGraph.valueHasMutated(); - } - - // realDeltaX - amount the mouse moved in X - // realDeltaY - amount the mouse moved in Y - // actualDeltaX - amount the moving object moved in X, may be different from realDeltaX if snap-to-grid is enabled - // actualDeltaY - amount the moving object moved in Y, may be different from realDeltaY if snap-to-grid is enabled - function moveChildNodes(node: Node, realDeltaX: number, realDeltaY: number, actualDeltaX: number, actualDeltaY: number) : void { - // get id of parent node - const parentKey : number = node.getKey(); - - // loop through all nodes, if they belong to the parent's group, move them too - for (const n of nodeData){ - // skip selected nodes, they are handled in the main drag code - if (eagle.objectIsSelected(n)){ - continue; - } - - if (n.getParentKey() === parentKey){ - moveNode(n, actualDeltaX, actualDeltaY); - moveChildNodes(n, realDeltaX, realDeltaY, actualDeltaX, actualDeltaY); - } - } - } - - function moveNode(node : Node, deltax : number, deltay : number) : void { - node.setPosition(getX(node) + deltax, getY(node) + deltay, false); - } - - function findAncestorCollapsedNode(node : Node) : Node { - let n : Node = node; - let iterations = 0; - - while (true){ - if (iterations > 32){ - console.error("too many iterations in findAncestorCollapsedNode()"); - return null; - } - - // debug - if (n.getKey() === n.getParentKey()){ - console.error("node", n.getKey(), "is own parent! parentKey", n.getParentKey(), n.getName()); - return null; - } - - iterations += 1; - - const oldKey : number = n.getKey(); - - // move up one level (preference using the node's embed key, then the parent key) - if (n.getEmbedKey() !== null){ - n = findNodeWithKey(n.getEmbedKey(), nodeData); - } else { - n = findNodeWithKey(n.getParentKey(), nodeData); - } - - // if node is null, return "inline" - if (n === null){ - return null; - } - else { - if (n.getKey() === oldKey){ - console.warn("move up did not move, aborting"); - return null; - } - - // if node is non-null, but collapsed, return "none" - if (n.isCollapsed()){ - return n; - } - } - - // otherwise continue while loop - } - } - - function isAncestor(node : Node, possibleAncestor : Node) : boolean { - let n : Node = node; - let iterations = 0; - - if (n === null){ - return false; - } - - while (true){ - if (iterations > 32){ - console.error("too many iterations in isDescendent()"); - return null; - } - - iterations += 1; - - // check if found - if (n.getKey() === possibleAncestor.getKey()){ - return true; - } - - // otherwise keep traversing upwards - const newKey = n.getParentKey(); - - // if we reach a null parent, we are done looking - if (newKey === null){ - return false; - } - - n = findNodeWithKey(newKey, nodeData); - } - } - - function getNodeDisplay(node : Node) : string { - // hide if node has collapsed ancestor - if (findAncestorCollapsedNode(node) !== null){ - return "none"; - } - - return "inline"; - } - - function getNodeRectDisplay(node: Node): string { - if (node.isBranch()){ - return "none"; - } - return "inline"; - } - - function getNodeCustomShapeDisplay(node: Node): string { - if (node.isBranch()){ - return "inline"; - } - return "none"; - } - - function getNodeCustomShapePoints(node: Node): string { - switch(node.getCategory()){ - case Category.Branch: - let half_width = 200 / 2; - let half_height = 100 / 2; - let offsetX = 0; - let offsetY = 0; - - // if branch is collapsed, reduce to half size - if (node.isCollapsed() && !node.isPeek()){ - half_width = 50; - half_height = 25; - offsetX = 50; - offsetY = 25; - } - - return (half_width+offsetX) + ", " + offsetY + " " + ((half_width*2)+offsetX) + ", " + (half_height+offsetY) + " " + (half_width+offsetX) + ", " + ((half_height*2)+offsetY) + " " + offsetX + ", " + (half_height+offsetY); - default: - return ""; - } - } - - function getResizeControlDisplay(node : Node) : string { - if (node.isCollapsed()){ - return "none"; - } - - return node.isResizable() ? "inline" : "none"; - } - - function getShrinkControlDisplay(node : Node) : string { - if (SHRINK_BUTTONS_ENABLED){ - if (node.isGroup()){ - return node.isCollapsed() ? "none" : "inline"; - } else { - return "none"; - } - } else { - return "none"; - } - } - - // whether or not an object in the graph should be rendered or not - function getPortsDisplay(node : Node) : string { - if (node.isCollapsed() && !node.isPeek()){ - return "none"; - } - - if (!node.isGroup() && node.isCollapsed() && !node.isPeek()){ - return "none"; - } - - return "inline"; - } - - function findNodesInRegion(left: number, right: number, top: number, bottom: number): Node[] { - const result: Node[] = []; - - // re-assign left, right, top, bottom in case selection region was not dragged in the typical NW->SE direction - const realLeft = left <= right ? left : right; - const realRight = left <= right ? right : left; - const realTop = top <= bottom ? top : bottom; - const realBottom = top <= bottom ? bottom : top; - - for (let i = nodeData.length - 1; i >= 0 ; i--){ - const node : Node = nodeData[i]; - - // use center of node as position - const centerX : number = node.getPosition().x + node.getWidth()/2; - const centerY : number = node.getPosition().y + node.getHeight()/2; - - if (centerX >= realLeft && realRight >= centerX && centerY >= realTop && realBottom >= centerY){ - result.push(node); - } - } - - return result; - } - - function findEdgesContainedByNodes(edges: Edge[], nodes: Node[]): Edge[]{ - const result: Edge[] = []; - - for (const edge of edges){ - const srcKey = edge.getSrcNodeKey(); - const destKey = edge.getDestNodeKey(); - let srcFound = false; - let destFound = false; - - for (const node of nodes){ - if ((node.getKey() === srcKey) || - (node.hasInputApplication() && node.getInputApplication().getKey() === srcKey) || - (node.hasOutputApplication() && node.getOutputApplication().getKey() === srcKey)){ - srcFound = true; - } - - if ((node.getKey() === destKey) || - (node.hasInputApplication() && node.getInputApplication().getKey() === destKey) || - (node.hasOutputApplication() && node.getOutputApplication().getKey() === destKey)){ - destFound = true; - } - } - - if (srcFound && destFound){ - result.push(edge); - } - } - - return result; - } - - function findNodesInRange(positionX: number, positionY: number, range: number, sourceNodeKey: number): Node[]{ - const result: Node[] = []; - - //console.log("findNodesInRange(): sourceNodeKey", sourceNodeKey); - - for (let i = 0; i < nodeData.length; i++){ - // skip the source node - if (nodeData[i].getKey() === sourceNodeKey){ - continue; - } - - // fetch categoryData for the node - const categoryData = CategoryData.getCategoryData(nodeData[i].getCategory()); - let possibleInputs = categoryData.maxInputs; - let possibleOutputs = categoryData.maxOutputs; - - // add categoryData for embedded apps (if they exist) - if (nodeData[i].hasInputApplication()){ - const inputApp = nodeData[i].getInputApplication(); - const inputAppCategoryData = CategoryData.getCategoryData(inputApp.getCategory()); - possibleInputs += inputAppCategoryData.maxInputs; - possibleOutputs += inputAppCategoryData.maxOutputs; - } - if (nodeData[i].hasOutputApplication()){ - const outputApp = nodeData[i].getOutputApplication(); - const outputAppCategoryData = CategoryData.getCategoryData(outputApp.getCategory()); - possibleInputs += outputAppCategoryData.maxInputs; - possibleOutputs += outputAppCategoryData.maxOutputs; - } - - // skip nodes that can't have inputs or outputs - if (possibleInputs === 0 && possibleOutputs === 0){ - continue; - } - - // determine distance from position to this node - const distance = Utils.positionToNodeDistance(positionX, positionY, nodeData[i]); - - if (distance <= range){ - //console.log("distance to", nodeData[i].getName(), nodeData[i].getKey(), "=", distance); - result.push(nodeData[i]); - } - } - - return result; - } - - function findNearestMatchingPort(positionX: number, positionY: number, nearbyNodes: Node[], sourceNode: Node, sourcePort: Field, sourcePortIsInput: boolean) : Field { - //console.log("findNearestMatchingPort(), sourcePortIsInput", sourcePortIsInput); - let minDistance = Number.MAX_SAFE_INTEGER; - let minPort = null; - - for (const node of nearbyNodes){ - let portList: Field[] = []; - - // if source node is Data, then no nearby Data nodes can have matching ports - if (sourceNode.getCategoryType() === Category.Type.Data && node.getCategoryType() === Category.Type.Data){ - continue; - } - - // if sourcePortIsInput, we should search for output ports, and vice versa - if (sourcePortIsInput){ - portList = portList.concat(node.getOutputPorts()); - } else { - portList = portList.concat(node.getInputPorts()); - } - - // get inputApplication ports - if (sourcePortIsInput){ - portList = portList.concat(node.getInputApplicationOutputPorts()); - } else { - portList = portList.concat(node.getInputApplicationInputPorts()); - } - - // get outputApplication ports - if (sourcePortIsInput){ - portList = portList.concat(node.getOutputApplicationOutputPorts()); - } else { - portList = portList.concat(node.getOutputApplicationInputPorts()); - } - - for (const port of portList){ - if (!Utils.portsMatch(port, sourcePort)){ - continue; - } - - // if port has no id (broken) then don't consider it as a auto-complete target - if (port.getId() === ""){ - continue; - } - - // get position of port - const portX = node.getPosition().x; - const portY = node.getPosition().y; - - // get distance to port - const distance = Math.sqrt( Math.pow(portX - positionX, 2) + Math.pow(portY - positionY, 2) ); - - // remember this port if it the best so far - if (distance < minDistance){ - minPort = port; - minDistance = distance; - } - } - } - - return minPort; - } - - function mouseEnterPort(port : Field) : void { - if (!isDraggingPort){ - return; - } - - destinationPort = port; - destinationNode = graph.findNodeByKey(port.getNodeKey()); - - isDraggingPortValid = Edge.isValid(eagle, null, sourceNode.getKey(), sourcePort.getId(), destinationNode.getKey(), destinationPort.getId(), sourcePort.getType(), false, false, false, false, {errors:[], warnings:[]}); - } - - function mouseLeavePort(port : Field) : void { - destinationPort = null; - destinationNode = null; - - isDraggingPortValid = Eagle.LinkValid.Unknown; - } - - function REAL_TO_DISPLAY_POSITION_X(x: number) : number { - return eagle.globalOffsetX + (x * eagle.globalScale); - } - function REAL_TO_DISPLAY_POSITION_Y(y: number) : number { - return eagle.globalOffsetY + (y * eagle.globalScale); - } - function REAL_TO_DISPLAY_SCALE(n: number, name: string = null) : number { - - if (name != null){ - console.log(name, n, eagle.globalScale, n * eagle.globalScale); - } - - return n * eagle.globalScale; - } - function DISPLAY_TO_REAL_POSITION_X(x: number) : number { - return (x - eagle.globalOffsetX)/eagle.globalScale; - } - function DISPLAY_TO_REAL_POSITION_Y(y: number) : number { - return (y - eagle.globalOffsetY)/eagle.globalScale; - } - function DISPLAY_TO_REAL_SCALE(n: number) : number { - return n / eagle.globalScale; - } - - function getWrapWidth(node: Node) { - if (node.isData()){ - return Number.POSITIVE_INFINITY; - } - - return node.getDisplayWidth(); - } - - function wrap(text: any, padding: boolean) { - text.each(function() { - const text = d3.select(this), - words = text.text().split(/[_ ]+/).reverse(), - lineHeight = 1.1, // ems - x = parseInt(text.attr("x"), 10), - y = parseInt(text.attr("y"), 10), - //dy = parseFloat(text.attr("dy")), - dy = 0.0; - let word; - let wordWrapWidth = parseInt(text.attr("eagle-wrap-width"), 10); - let line : string[] = []; - let tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em"); - $(this).attr('transform','translate(0,-3)'); - let lineNumber = 0; - - if (padding){ - wordWrapWidth = wordWrapWidth - x - x; - } - - while (word = words.pop()) { - line.push(word); - tspan.text(line.join(" ")); - if (tspan.node().getComputedTextLength() > wordWrapWidth) { - line.pop(); - tspan.text(line.join(" ")); - line = [word]; - tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word); - $(this).attr('transform','translate(0,-15)') - } - } - }); - } - - // performance - const elapsedTime = performance.now() - startTime; - if (elapsedTime > eagle.rendererFrameMax){eagle.rendererFrameMax = elapsedTime;} - eagle.rendererFrameDisplay("render " + elapsedTime.toFixed(2) + "ms (max " + eagle.rendererFrameMax.toFixed(2) + "ms) Renders " + eagle.rendererFrameCountRender + " Ticks " + eagle.rendererFrameCountTick); -} diff --git a/src/graphConfig.ts b/src/graphConfig.ts new file mode 100644 index 000000000..4112d3bc9 --- /dev/null +++ b/src/graphConfig.ts @@ -0,0 +1,121 @@ +const colors: { name: string; color: string; }[] = [ + { + //node colours + name: 'bodyBorder', + color: '#2e3192' + },{ + name: 'branchBg', + color: '#dcdee2' + },{ + name: 'constructBg', + color: '#05142912' + },{ + name: 'embeddedApp', + color: '#dcdee2' + },{ + name: 'constructIcon', + color: '#0000000f' + },{ + name: 'graphText', + color: 'black' + },{ + name: 'nodeBg', + color: 'white' + },{ + name: 'nodeInputPort', + color: '#2bb673' + },{ + name: 'nodeOutputPort', + color: '#fbb040' + },{ + name: 'nodeUtilPort', + color: '#6fa7f1' + },{ + name: 'selectBackground', + color: '#b4d4ff' + },{ + name: 'selectConstructBackground', + color: '#85b9ff94' + },{ + + //edge colours + name: 'edgeDefault', + color: '#58595b' + },{ + name: 'edgeDefaultSelected', + color: '#4247df' + },{ + name: 'commentEdge', + color: '#7c7e81' + },{ + name: 'edgeValid', + color: '#32cd32' + },{ + name: 'edgeWarning', + color: '#ffa500' + },{ + name: 'edgeWarningSelected', + color: '#4247df' + },{ + name: 'edgeInvalid', + color: '#ff0000' + },{ + name: 'edgeInvalidSelected', + color: '#4247df' + },{ + name: 'edgeEvent', + color: '#a6a6fe' + },{ + name: 'edgeEventSelected', + color: '#4247df' + },{ + name: 'edgeAutoCompleteSuggestion', + color: '#dbcfe1' + },{ + name: 'edgeAutoComplete', + color: '#9c3bca' + },{ + name: 'edgeClosesLoop', + color: '#58595b' + },{ + name: 'edgeClosesLoopSelected', + color: '#4247df' + } +] + +export class GraphConfig { + + // graph behaviour + public static readonly NODE_SUGGESTION_RADIUS = 300 + public static readonly NODE_SUGGESTION_SNAP_RADIUS = 150 + public static readonly PORT_MINIMUM_DISTANCE = 14 + + //node settings + // TODO: could move to CategoryData? + public static readonly NORMAL_NODE_RADIUS : number = 25; + public static readonly BRANCH_NODE_RADIUS : number = 44; + public static readonly CONSTRUCT_NODE_RADIUS: number = 200; + public static readonly MINIMUM_CONSTRUCT_RADIUS : number = 44; + + //edge settings + public static readonly EDGE_ARROW_SIZE : number = 8; + public static readonly EDGE_DISTANCE_ARROW_VISIBILITY : number = 100; //how loong does an edge have to be to show the direction arrows + public static readonly SWITCH_TO_STRAIGHT_EDGE_MULTIPLIER : number = 5 //this affect the cutoff distance between nodes required to switch between a straight and curved edge + + // when creating a new construct to enclose a selection, or shrinking a node to enclose its children, + // this is the default margin that should be left on each side + public static readonly CONSTRUCT_MARGIN: number = 30; + public static readonly CONSTRUCT_DRAG_OUT_DISTANCE: number = 200; + + static getColor = (name:string) : string => { + let result = 'red' + for (const color of colors) { + if(color.name === name){ + result = color.color + }else{ + continue + } + } + return result + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 93625d276..0b689812d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,15 +28,20 @@ import "jqueryMigrate"; import "jqueryui"; import * as bootstrap from 'bootstrap'; -import {UiMode, UiModeSystem, SettingData} from './UiModes'; +import { ActionList } from "./ActionList"; +import { ActionMessage } from "./Action"; import {Category} from './Category'; import {CategoryData} from './CategoryData'; import {Config} from './Config'; import {Daliuge} from './Daliuge'; import {Eagle} from './Eagle'; -import {Errors} from './Errors'; import {GitHub} from './GitHub'; import {GitLab} from './GitLab'; +<<<<<<< HEAD +import { GraphChecker } from "./GraphChecker"; +======= +import { GraphRenderer } from "./GraphRenderer"; +>>>>>>> html-graph-renderer import {Hierarchy} from './Hierarchy'; import {RightClick} from './RightClick'; import {QuickActions} from './QuickActions'; @@ -45,6 +50,7 @@ import {LogicalGraph} from './LogicalGraph'; import {Modals} from './Modals'; import {Palette} from './Palette'; import {Setting} from './Setting'; +import {UiMode, UiModeSystem, SettingData} from './UiModes'; import {Utils} from './Utils'; import {Repositories} from './Repositories'; import {Repository} from './Repository'; @@ -52,6 +58,7 @@ import {RepositoryFile} from './RepositoryFile'; import {ParameterTable} from "./ParameterTable"; import {SideWindow} from "./SideWindow"; import {TutorialSystem} from "./Tutorial"; +import {GraphConfig} from "./graphConfig"; import * as quickStart from './tutorials/quickStart' import * as graphBuilding from './tutorials/graphBuilding' @@ -68,11 +75,12 @@ $(function(){ // add eagle to the window object, slightly hacky, but useful for debugging (window).eagle = eagle; + (window).ActionList = ActionList; (window).Category = Category; (window).Config = Config; (window).Daliuge = Daliuge; (window).Eagle = Eagle; - (window).Errors = Errors; + (window).GraphChecker = GraphChecker; (window).Hierarchy = Hierarchy; (window).ParameterTable = ParameterTable; (window).Repositories = Repositories; @@ -80,11 +88,17 @@ $(function(){ (window).Setting = Setting; (window).SideWindow = SideWindow; (window).TutorialSystem = TutorialSystem; +<<<<<<< HEAD + (window).ActionMessage = ActionMessage; +======= + (window).GraphRenderer = GraphRenderer; +>>>>>>> html-graph-renderer (window).UiModeSystem = UiModeSystem; (window).Utils = Utils; (window).KeyboardShortcut = KeyboardShortcut; (window).QuickActions = QuickActions; (window).Modals = Modals; + (window).GraphConfig = GraphConfig; ko.options.deferUpdates = true; ko.applyBindings(eagle); @@ -123,32 +137,67 @@ $(function(){ GitLab.loadRepoList(); } - // load the default palette + // build list of auto-load files + const autoLoadFiles: RepositoryFile[] = []; + + // if url contains file to auto-load, add to auto-load files list + const urlAutoLoadFile = parseUrlAutoLoad(); + if (urlAutoLoadFile !== null){ + autoLoadFiles.push(urlAutoLoadFile); + } + + // if 'load default palette' setting is set if (Setting.findValue(Setting.OPEN_DEFAULT_PALETTE)){ - eagle.loadPalettes([ - {name:"Builtin Components", filename:Daliuge.PALETTE_URL, readonly:true}, - {name:Palette.DYNAMIC_PALETTE_NAME, filename:Daliuge.TEMPLATE_URL, readonly:true} - ], (errorsWarnings: Errors.ErrorsWarnings, palettes: Palette[]):void => { - const showErrors: boolean = Setting.findValue(Setting.SHOW_FILE_LOADING_ERRORS); - - // display of errors if setting is true - if (showErrors && (Errors.hasErrors(errorsWarnings) || Errors.hasWarnings(errorsWarnings))){ - // add warnings/errors to the arrays - eagle.loadingErrors(errorsWarnings.errors); - eagle.loadingWarnings(errorsWarnings.warnings); - - eagle.errorsMode(Setting.ErrorsMode.Loading); - Utils.showErrorsModal("Loading File"); + autoLoadFiles.push(new RepositoryFile(new Repository(Eagle.RepositoryService.Url, "", "", false), Daliuge.PALETTE_URL, "Builtin Components")); + autoLoadFiles.push(new RepositoryFile(new Repository(Eagle.RepositoryService.Url, "", "", false), Daliuge.TEMPLATE_URL, Palette.DYNAMIC_PALETTE_NAME)); + } + + // load the default palette + eagle.loadFiles(autoLoadFiles, (palettes: {file: RepositoryFile, palette: Palette, errors: ActionMessage[]}[], logicalGraphs: {file: RepositoryFile, logicalGraph: LogicalGraph, errors: ActionMessage[]}[]):void => { + const loads : {file: RepositoryFile, errors: ActionMessage[]}[] = []; + + // handle palettes + for (const p of palettes){ + loads.push({file:p.file, errors: p.errors}); + if (p.palette !== null){ + p.palette.fileInfo().name = p.file.name; + eagle.remotePaletteLoaded(p.file, p.palette); } + } - for (const palette of palettes){ - if (palette !== null){ - eagle.palettes.push(palette); - } + // handle graphs + for (const g of logicalGraphs){ + loads.push({file:g.file, errors: g.errors}); + if (g.logicalGraph !== null){ + eagle.remoteGraphLoaded(g.file, g.logicalGraph); } + } + + // handle errors + eagle.handleLoadingErrors(loads); + + // show the left window if palettes were loaded + // TODO: is this required? try removing + if (palettes.length > 0){ eagle.leftWindow().shown(true); - }); - } + } + + if (logicalGraphs.length > 0){ + // center graph + eagle.centerGraph(); + + // check graph + eagle.graphChecker().check(); + + // HACK: we assume the urlAutoLoadFile is the graph file, may not be the case! + + // push undo snapshot + eagle.undo().pushSnapshot(eagle, "Loaded " + urlAutoLoadFile.name); + + // if the fileType is the same as the current mode, update the activeFileInfo with details of the repository the file was loaded from + eagle.updateLogicalGraphFileInfo(urlAutoLoadFile); + } + }); // set other state based on settings values if (Setting.findValue(Setting.SNAP_TO_GRID)){ @@ -171,15 +220,17 @@ $(function(){ document.onkeydown = KeyboardShortcut.processKey; document.onkeyup = KeyboardShortcut.processKey; +<<<<<<< HEAD // HACK: without this global wheel event handler, d3 does not receive zoom events // not sure why, this wasn't always the case document.onwheel = () => {return;}; - +======= // auto load the file autoLoad(eagle); +>>>>>>> html-graph-renderer // auto load a tutorial, if specified on the url - autoTutorial(eagle); + parseUrlAutoTutorial(); //hides the dropdown navbar elements when stopping hovering over the element $(".dropdown-menu").mouseleave(function(){ @@ -190,9 +241,9 @@ $(function(){ $('.modal').on('hidden.bs.modal', function () { $('.modal-dialog').css({"left":"0px", "top":"0px"}) $("#editFieldModal textarea").attr('style','') - $("#errorsModalAccordion").parent().parent().attr('style','') + $("#checkGraphModalAccordion").parent().parent().attr('style','') - //reset parameter table selecction + //reset parameter table selection ParameterTable.resetSelection() }); @@ -274,9 +325,17 @@ $(function(){ break; } } + + + //initiating all the eagle ui when the graph is ready + $('#logicalGraph').show(200) + $('.leftWindow').show(200) + $('.rightWindow').show(200) + $('#graphNameWrapper').show(200) + $('nav.navbar').show(200).css('display', 'flex'); }); -function autoLoad(eagle: Eagle) { +function parseUrlAutoLoad(): RepositoryFile { const service = (window).auto_load_service; const repository = (window).auto_load_repository; const branch = (window).auto_load_branch; @@ -290,30 +349,30 @@ function autoLoad(eagle: Eagle) { // skip unknown services if (typeof realService === "undefined" || realService === Eagle.RepositoryService.Unknown){ console.log("No auto load. Service Unknown"); - return; + return null; } // skip empty strings if ([Eagle.RepositoryService.GitHub, Eagle.RepositoryService.GitLab].includes(realService) && (repository === "" || branch === "" || filename === "")){ console.log("No auto load. Repository, branch or filename not specified"); - return; + return null; } // skip url if url is not specified if (realService === Eagle.RepositoryService.Url && url === ""){ console.log("No auto load. Url not specified"); - return; + return null; } // load if (service === Eagle.RepositoryService.Url){ - Repositories.selectFile(new RepositoryFile(new Repository(service, "", "", false), "", url)); + return new RepositoryFile(new Repository(service, "", "", false), url, ""); } else { - Repositories.selectFile(new RepositoryFile(new Repository(service, repository, branch, false), path, filename)); + return new RepositoryFile(new Repository(service, repository, branch, false), path, filename); } } -function autoTutorial(eagle: Eagle){ +function parseUrlAutoTutorial(){ const urlParams = new URLSearchParams(window.location.search); const tutorialName = urlParams.get('tutorial'); diff --git a/src/require-config.ts b/src/require-config.ts index aef63f2d0..01da8b77b 100644 --- a/src/require-config.ts +++ b/src/require-config.ts @@ -9,12 +9,10 @@ require.config({ "jqueryui": "./static/externals/jquery-ui.min", "bootstrap": "./static/externals/bootstrap.bundle.min", "bootstrap-notify": "./static/externals/bootstrap-notify.min", - "d3": "./static/externals/d3.v5.min", "ajv": "./static/externals/ajv.min", "showdown": "./static/externals/showdown.min", "bindingHandlers/readonly":"./static/built/bindingHandlers/readonly", "bindingHandlers/disabled":"./static/built/bindingHandlers/disabled", - "bindingHandlers/graphRenderer":"./static/built/bindingHandlers/graphRenderer", "bindingHandlers/eagleTooltip":"./static/built/bindingHandlers/eagleTooltip", "bindingHandlers/eagleRightClick":"./static/built/bindingHandlers/eagleRightClick", "components":"./static/built/components", @@ -26,7 +24,9 @@ require.config({ "Eagle": "./static/built/Eagle", "Utils": "./static/built/Utils", "Modals": "./static/built/Modals", + "GraphChecker": "./static/built/GraphChecker", "GraphUpdater": "./static/built/GraphUpdater", + "GraphRenderer": "./static/built/GraphRenderer", "Repository": "./static/built/Repository", "RepositoryFolder": "./static/built/RepositoryFolder", "RepositoryFile": "./static/built/RepositoryFile", @@ -56,7 +56,13 @@ require.config({ "Hierarchy": "./static/built/Hierarchy", "RightClick": "./static/built/RightClick", "Repositories": "./static/built/Repositories", - "ParameterTable": "./static/built/ParameterTable" + "ParameterTable": "./static/built/ParameterTable", +<<<<<<< HEAD + "Action": "./static/built/Action", + "ActionList": "./static/built/ActionList" +======= + "graphConfig": "./static/built/graphConfig" +>>>>>>> html-graph-renderer }, shim: { "bootstrap": { diff --git a/src/tutorials/graphBuilding.ts b/src/tutorials/graphBuilding.ts index fab621c9c..3f179ae0c 100644 --- a/src/tutorials/graphBuilding.ts +++ b/src/tutorials/graphBuilding.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import {Tutorial, TutorialStep, TutorialSystem} from '../Tutorial'; import {Eagle} from '../Eagle'; @@ -120,15 +121,15 @@ newTut.newTutStep("Connecting nodes", "Click and hold the output Port of the newTut.newTutStep("Graph Errors and warnings", "Notice that we have a few graph warnings detected. Click here to view them", function(){return $("#checkGraphWarnings")}) .setType(TutorialStep.Type.Press) -newTut.newTutStep("Graph Errors and warnings", "This modal may aid you in troubleshooting graphs. In this case these errors are all port type errors. Eagle can automatically fix errors such as these for you. To do this you can press 'F' in the graph or click on 'Fix All'", function(){return $("#errorModalFixAll")}) +newTut.newTutStep("Graph Errors and warnings", "This modal may aid you in troubleshooting graphs. In this case these errors are all port type errors. Eagle can automatically fix errors such as these for you. To do this you can press 'F' in the graph or click on 'Fix All'", function(){return $("#checkGraphModalFixAll")}) .setType(TutorialStep.Type.Press) .setWaitType(TutorialStep.Wait.Modal) -.setAlternateHighlightTargetFunc(function(){return $("#errorModalFixAll").parent().parent()}) -.setBackPreFunction(function(){$('#errorsModal').modal('show')}) +.setAlternateHighlightTargetFunc(function(){return $("#checkGraphModalFixAll").parent().parent()}) +.setBackPreFunction(function(eagle:Eagle){eagle.openCheckGraphModal()}) newTut.newTutStep("Saving a Graph", "Options to save your graph are available in the graph menu Click on 'Graph' to continue.", function(){return $("#navbarDropdownGraph")}) .setType(TutorialStep.Type.Press) -.setPreFunction(function(eagle:Eagle){eagle.closeErrorsModal()}) +.setPreFunction(function(eagle:Eagle){eagle.closeCheckGraphModal()}) .setBackPreFunction(function(){$('.forceShow').removeClass('forceShow');$(".dropdown-toggle").removeClass("show");$(".dropdown-menu").removeClass("show")}) //allowing the graph navbar dropdown to hide newTut.newTutStep("Saving a Graph", "You are able to download the graph in the 'local storage' section, or save the graph into your github repository under 'git storage'", function(){return $("#navbarDropdownGraph").parent().find('.dropdown-menu')}) @@ -137,3 +138,144 @@ newTut.newTutStep("Saving a Graph", "You are able to download the graph in the ' newTut.newTutStep("Well Done!", "You have completed the Hello world graph creation tutorial! Be sure to check our online documentation for additional help and guidance.", function(){return $("#logicalGraphParent")}) .setPreFunction(function(){$('.forceShow').removeClass('forceShow')}) //allowing the graph navbar dropdown to hide +======= +import { Eagle } from '../Eagle'; +import { RightClick } from '../RightClick'; +import { TutorialStep, TutorialSystem } from '../Tutorial'; + + +const newTut = TutorialSystem.newTutorial('Graph Building', 'An introduction to graph building.') + +newTut.newTutStep("Welcome to the Graph Building tutorial!", "You can quit this tutorial anytime using the 'exit' button or ESC key. Please refer to the main documentation for in-depth information.", function(){return $("#logicalGraphParent")}) + +newTut.newTutStep("Creating a New Graph", "First we are going to create a new graph. Options for creating, loading and saving graphs can be found here. Click on 'Graph' to continue.", function(){return $("#navbarDropdownGraph")}) +.setType(TutorialStep.Type.Press) +.setBackPreFunction(function(){$('.forceShow').removeClass('forceShow');$('.modal').modal("hide");}) //allowing the graph navbar dropdown to hide + +newTut.newTutStep("Creating a New Graph", "Click on 'New'.", function(){return $("#navbarDropdownGraph").parent().find('.dropdown-item').first()}) +.setType(TutorialStep.Type.Press) +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')}) //keeping the navbar graph doropdown open +.setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('#createNewGraph').removeClass('forceShow')})//allowing the 'new' drop drop down section to close +.setBackSkip(true) + +newTut.newTutStep("Creating a New Graph", "Click on 'Create new graph'", function(){return $("#navbarDropdownGraph").parent().find('#createNewGraph')}) +.setType(TutorialStep.Type.Press) +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')})//keeping the 'new' drop drop down section open as well +.setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('.dropdown-item').first().parent().addClass('forceShow');TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')})//force showing both of the navbar graph drop downs +.setBackSkip(true) + +newTut.newTutStep("Creating a new graph", "Then just give it a name and press enter", function(){return $("#inputModalInput")}) +.setWaitType(TutorialStep.Wait.Modal) +.setType(TutorialStep.Type.Input) +.setPreFunction(function(){$('.forceShow').removeClass('forceShow')}) //allowing the graph navbar dropdown to hide +.setBackSkip(true) + +newTut.newTutStep("Creating a new graph", "And 'Ok' to save!", function(){return $("#inputModal .affermativeBtn")}) +.setWaitType(TutorialStep.Wait.Modal) +.setType(TutorialStep.Type.Press) +.setBackSkip(true) + +newTut.newTutStep("Graph Model Data", "This button brings up the 'Graph Modal Data' which allows you to add a description for your graph. Try clicking it now to try it out", function(){return $("#openGraphModelDataModal")}) +.setType(TutorialStep.Type.Press) +.setBackPreFunction(function(){$('.modal').modal("hide");}) //hiding open modals + +newTut.newTutStep("Editing Graph Descriptions", "You are able to enter a simple first glance and a more detailed decription in addition to description nodes in the graph, should you need it.", function(){return $("#modelDataDescription")}) +.setWaitType(TutorialStep.Wait.Modal) + +newTut.newTutStep("Other Graph Information", "Most of the other information is automatically filled out when saving a graph, such as the version of EAGLE used for creating it.", function(){return $("#modelDataEagleVersion")}) +.setWaitType(TutorialStep.Wait.Modal) + +newTut.newTutStep("Close the Modal", "Press OK to close the modal and continue the Tutorial.", function(){return $("#modelDataModalOKButton")}) +.setWaitType(TutorialStep.Wait.Modal) +.setType(TutorialStep.Type.Press) +.setBackPreFunction(function(){$('#modelDataModal').modal('show')}) + +newTut.newTutStep("Palette Components", "Each of these components in a palette performs a function that can be used in your graph", function(){return $("#palette_0_HelloWorldApp")}) + +newTut.newTutStep("Adding base components into the graph", "To add one into the graph, simply click on the icon or drag the component into the graph. Click on the icon to continue.", function(){return $("#addPaletteNodeHelloWorldApp")}) +.setType(TutorialStep.Type.Press) + +newTut.newTutStep("Graph Nodes", "Once added into your graph, the component is in your own instance. This means you can adjust its parameters and they will be saved with the graph. Click on the node to select it.", function(){return TutorialSystem.initiateFindGraphNodeIdByNodeName('HelloWorldApp')}) +.setType(TutorialStep.Type.Condition) +.setWaitType(TutorialStep.Wait.Element) +.setConditionFunction(function(){return TutorialSystem.isRequestedNodeSelected('HelloWorldApp')}) +.setPreFunction(function(eagle:Eagle){eagle.resetEditor()}) +.setBackPreFunction(function(eagle:Eagle){eagle.resetEditor()}) + +newTut.newTutStep("Editing Components", "The inspector panel provides access to the complete set of specifications of a component. The Component Parameters are settings pertaining to the DALiuGE component wrapper, the Application Arguments are settings exposed by the actual application code.", function(){return $("#rightWindowContainer")}) +.setPreFunction(function(eagle:Eagle){eagle.rightWindow().mode(Eagle.RightWindowMode.Inspector)}) + +newTut.newTutStep("Click to open", "Click to open the node fields table and continue.", function(){return $("#openNodeFieldsTable")}) +.setWaitType(TutorialStep.Wait.Element) +.setType(TutorialStep.Type.Press) +.setBackPreFunction(function(){$('#parameterTableModal').modal('hide')}) + +newTut.newTutStep("Enter a Name", "In case of this hello world app we can change who we are greeting. Enter a name and press enter to continue.", function(){return $('.tableFieldStringValueInput').first()}) +.setType(TutorialStep.Type.Input) +.setWaitType(TutorialStep.Wait.Delay) +.setDelayAmount(700) + +newTut.newTutStep("Key Attributes", "You can flag important parameters and attributes of a graph as 'Key Attributes'. These are then all available for editing in one location. Click on the heart to flag this argument as key attribute.", function(){return $('.column_KeyAttr button').first()}) +.setType(TutorialStep.Type.Press) +.setBackPreFunction(function(){$('#openNodeFieldsTable').click()}) +.setWaitType(TutorialStep.Wait.Delay) +.setDelayAmount(700) + +newTut.newTutStep("Key Attributes", "You can view the key attributes of a graph by opening the key attributes table located here.", function(){return $("#openKeyParameterTable")}) +.setPreFunction(function(){$('#parameterTableModal').modal('hide')}) + +newTut.newTutStep("Right Click to add nodes", "There are also various right click options available in EAGLE. Right click on the graph to bring up a 'add node' menu", function(){return $("#logicalGraphParent")}) +.setType(TutorialStep.Type.Condition) +.setConditionFunction(function(){ if($('#customContextMenu').length){return true}else{return false}}) +.setPreFunction(function(){$('.modal').modal("hide");}) //hiding open moddals +.setBackPreFunction(function(){RightClick.closeCustomContextMenu(true);}) + +newTut.newTutStep("Graph Context menu", "all of your loaded palettes and their contents will appear here", function(){return $("#rightClickPaletteList")}) +.setPreFunction(function(){$("#customContextMenu").addClass('forceShow')}) +.setWaitType(TutorialStep.Wait.Element) +.setBackSkip(true) + +newTut.newTutStep("Quickly adding nodes", "If you already know what you want you can quickly add it by using the search bar. Search for 'file' now and press enter", function(){return $("#rightClickSearchBar")}) +.setType(TutorialStep.Type.Input) +.setExpectedInput('file') +.setBackSkip(true) + +newTut.newTutStep("Connecting nodes", "To save the output of the hello world app onto the file we need to draw an edge from the 'Hello World' node's output port to the 'File' node's input port.", function(){return $("#logicalGraphParent")}) + +newTut.newTutStep("Node Ports", "This is the output port of the Hello world app, Output ports are always shown in orange and are initially on the right side of the node.", function(){return $('#portContainer .' + TutorialSystem.initiateSimpleFindGraphNodeIdByNodeName('HelloWorldApp')+' .outputPort')}) +.setPreFunction(function(eagle:Eagle){eagle.resetEditor()}) +.setBackPreFunction(function(eagle:Eagle){eagle.resetEditor()}) +.setAlternateHighlightTargetFunc(function(){return TutorialSystem.initiateFindGraphNodeIdByNodeName('HelloWorldApp')}) +.setWaitType(TutorialStep.Wait.Element) + +newTut.newTutStep("Node Ports", "And this is the input port for the file storage node, Iutput ports are always shown in green and are initially on the left side of the node.", function(){return $('#portContainer .' + TutorialSystem.initiateSimpleFindGraphNodeIdByNodeName('File')+' .inputPort')}) +.setPreFunction(function(eagle:Eagle){eagle.resetEditor()}) +.setBackPreFunction(function(eagle:Eagle){eagle.resetEditor()}) +.setAlternateHighlightTargetFunc(function(){return TutorialSystem.initiateFindGraphNodeIdByNodeName('File')}) +.setWaitType(TutorialStep.Wait.Element) + +newTut.newTutStep("Connecting nodes", "Click and hold the output Port of the hello world app and drag over to the file node's input port, then release.", function(){return $('#portContainer .' + TutorialSystem.initiateSimpleFindGraphNodeIdByNodeName('HelloWorldApp')+' .outputPort')}) +.setType(TutorialStep.Type.Condition) +.setAlternateHighlightTargetFunc(function(){return $("#logicalGraphParent")}) +.setConditionFunction(function(eagle:Eagle){if(eagle.logicalGraph().getEdges().length != 0){return true}else{return false}}) //check if there are any edges present in the graph + +newTut.newTutStep("Graph Errors and warnings", "This is the error checking system, it is showing a checkmark, so we did everything correctly. If there are errors in the graph you are able to troubleshoot them by clicking here.", function(){return $("#checkGraphDone")}) + +// newTut.newTutStep("Graph Errors and warnings", "This modal may aid you in troubleshooting graphs. In this case these errors are all port type errors. Eagle can automatically fix errors such as these for you. To do this you can press 'F' in the graph or click on 'Fix All'", function(){return $("#errorModalFixAll")}) +// .setType(TutorialStep.Type.Press) +// .setWaitType(TutorialStep.Wait.Modal) +// .setAlternateHighlightTargetFunc(function(){return $("#errorModalFixAll").parent().parent()}) +// .setBackPreFunction(function(){$('#errorsModal').modal('show')}) + +newTut.newTutStep("Saving a Graph", "Options to save your graph are available in the graph menu Click on 'Graph' to continue.", function(){return $("#navbarDropdownGraph")}) +.setType(TutorialStep.Type.Press) +.setPreFunction(function(eagle:Eagle){eagle.closeErrorsModal()}) +.setBackPreFunction(function(){$('.forceShow').removeClass('forceShow');$(".dropdown-toggle").removeClass("show");$(".dropdown-menu").removeClass("show")}) //allowing the graph navbar dropdown to hide + +newTut.newTutStep("Saving a Graph", "You are able to download the graph in the 'local storage' section, or save the graph into your github repository under 'git storage'", function(){return $("#navbarDropdownGraph").parent().find('.dropdown-menu')}) +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().addClass('forceShow')}) //keeping the navbar graph doropdown open +.setBackSkip(true) + +newTut.newTutStep("Well Done!", "You have completed the Hello world graph creation tutorial! Be sure to check our online documentation for additional help and guidance.", function(){return $("#logicalGraphParent")}) +.setPreFunction(function(){$('.forceShow').removeClass('forceShow')}) //allowing the graph navbar dropdown to hide +>>>>>>> html-graph-renderer diff --git a/static/base.css b/static/base.css index 9d95d33a2..d957015b8 100644 --- a/static/base.css +++ b/static/base.css @@ -195,13 +195,14 @@ color: #00bb00;} .rightWindow { background-color: rgba(214, 219, 239, 0.95); - z-index: 1; + z-index: 10; position:absolute; top: 0px; right: -300px; height: 100%; transition: right 0.25s linear; border-left: 2px solid rgba(0, 36, 74, 0.95); + display: none; } .rightWindow.show { @@ -410,7 +411,7 @@ color: #00bb00;} overflow-y: auto; } -#parameterTableModal .modal-dialog .typesInput, #edintFieldModal .modal-dialog .typesInput{ +#parameterTableModal .modal-dialog .typesInput, #editFieldModal .modal-dialog .typesInput{ padding: 0px; height: 35px; width: 100%; @@ -945,6 +946,7 @@ td:first-child input { height: 100%; transition: left 0.25s linear; padding:0px 2px 0px 6px; + display: none; } /* width */ @@ -1232,29 +1234,29 @@ select.form-control{ border: 1px solid #c2c8dc; } -#errorsModalAccordion .accordion-button{ +#checkGraphModalAccordion .accordion-button, #actionListModalAccordion .accordion-button { color: white; background-color: #4a6b8f; } -#errorsModalAccordion .accordion-button::after{ +#checkGraphModalAccordion .accordion-button::after, #actionListModalAccordion .accordion-button::after { background-image: url("data:image/svg+xml,") !important; } -#errorsModalAccordion .accordion-button:focus{ +#checkGraphModalAccordion .accordion-button:focus, #actionListModalAccordion .accordion-button:focus { border-color: transparent; box-shadow: none !important; } -#errorsModalAccordion .accordion-item{ +#checkGraphModalAccordion .accordion-item, #actionListModalAccordion .accordion-item { margin-bottom: 10px; } -#errorsModalErrorCount{ +#checkGraphModalErrorCount, #actionListModalErrorCount { color: #dc3545; } -#errorsModalWarningCount{ +#checkGraphModalWarningCount, #actionListModalWarningCount { color: #ffc107; } @@ -1872,20 +1874,13 @@ select.form-control{ height: 100%; } -#logicalGraphD3Div { +#logicalGraph { position: relative; width: 100%; height: 100%; background-color: white; } -#paletteD3Div { - position: relative; - width: 100%; - height: 100%; - background-color: #dddddd; -} - .navbar-btn i.material-icons.md-18 { position: relative; top: 3px; @@ -2070,6 +2065,7 @@ ul.nav.navbat-nav .btn-outline-secondary:hover{ padding-left:10px; background: rgb(0,64,133); background: linear-gradient(90deg, rgba(0,64,133,1) 0%, rgba(0,52,107,1) 20%, rgba(0,35,73,1) 100%); + display:none; } .navbar a{ @@ -2102,6 +2098,11 @@ ul.nav.navbat-nav .btn-outline-secondary:hover{ white-space: pre; } + #eagleAndVersion sup{ + font-family: 'EB Garamond', serif; + font-size: 1em; + } + #brandEagleIcon{ margin-left:4px; } @@ -2362,7 +2363,7 @@ ul.nav.navbar-nav .dropdown-item:hover{ #graphNameWrapper{ top: 56px; - display: block; + display: none; width: 100%; height:26px; background-color: #002349; @@ -2661,7 +2662,7 @@ palette-component input.form-control.selected { } .tooltip-inner h3 { - font-size: 1.2rem; + font-size: 18px; font-weight: 600; margin: 0px; } @@ -2692,12 +2693,12 @@ palette-component input.form-control.selected { transition: 0s visibility; visibility: visible; max-height: 400px; - max-width:300px; + max-width: 300px; overflow: auto; text-align: left; } -#errorsModal ul.list-group .list-group-item span { +#checkGraphModal ul.list-group .list-group-item span, #actionListModal ul.list-group .list-group-item span { max-width: 80%; } diff --git a/static/components/fix.html b/static/components/fix.html index 79a0b3014..732e8deb7 100644 --- a/static/components/fix.html +++ b/static/components/fix.html @@ -3,7 +3,7 @@
- +
@@ -13,7 +13,7 @@
- +
diff --git a/static/components/hierarchy-node.html b/static/components/hierarchy-node.html index ea320def4..71652525c 100644 --- a/static/components/hierarchy-node.html +++ b/static/components/hierarchy-node.html @@ -1,6 +1,6 @@
  • -
    +
    visibility_off @@ -14,7 +14,7 @@
    -
    +
    diff --git a/static/components/palette-component.html b/static/components/palette-component.html index 4bb96c3f9..1aa9c06a9 100644 --- a/static/components/palette-component.html +++ b/static/components/palette-component.html @@ -1,15 +1,15 @@
    -
    +
    -
    - +
    diff --git a/static/externals/d3.v5.min.js b/static/externals/d3.v5.min.js deleted file mode 100644 index 1309c949e..000000000 --- a/static/externals/d3.v5.min.js +++ /dev/null @@ -1,2 +0,0 @@ -// https://d3js.org v5.9.7 Copyright 2019 Mike Bostock -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t,n){return tn?1:t>=n?0:NaN}function e(t){var e;return 1===t.length&&(e=t,t=function(t,r){return n(e(t),r)}),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}var r=e(n),i=r.right,o=r.left;function a(t,n){return[t,n]}function u(t){return null===t?NaN:+t}function c(t,n){var e,r,i=t.length,o=0,a=-1,c=0,f=0;if(null==n)for(;++a1)return f/(o-1)}function f(t,n){var e=c(t,n);return e?Math.sqrt(e):e}function s(t,n){var e,r,i,o=t.length,a=-1;if(null==n){for(;++a=e)for(r=i=e;++ae&&(r=e),i=e)for(r=i=e;++ae&&(r=e),i0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(o>=y?10:o>=_?5:o>=b?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=y?10:o>=_?5:o>=b?2:1)}function w(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=y?i*=10:o>=_?i*=5:o>=b&&(i*=2),n=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),a=+e(t[o],o,t);return a+(+e(t[o+1],o+1,t)-a)*(i-o)}}function A(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++or&&(r=e)}else for(;++o=e)for(r=e;++or&&(r=e);return r}function T(t){for(var n,e,r,i=t.length,o=-1,a=0;++o=0;)for(n=(r=t[i]).length;--n>=0;)e[--a]=r[n];return e}function S(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++oe&&(r=e)}else for(;++o=e)for(r=e;++oe&&(r=e);return r}function k(t){if(!(i=t.length))return[];for(var n=-1,e=S(t,E),r=new Array(e);++n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}})),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),V.hasOwnProperty(n)?{space:V[n],local:t}:t}function W(t){var n=$(t);return(n.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===G&&n.documentElement.namespaceURI===G?n.createElement(t):n.createElementNS(e,t)}})(n)}function Z(){}function Q(t){return null==t?Z:function(){return this.querySelector(t)}}function J(){return[]}function K(t){return null==t?J:function(){return this.querySelectorAll(t)}}function tt(t){return function(){return this.matches(t)}}function nt(t){return new Array(t.length)}function et(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}et.prototype={constructor:et,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var rt="$";function it(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function ut(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function ct(t,n){return t.style.getPropertyValue(n)||ut(t).getComputedStyle(t,null).getPropertyValue(n)}function ft(t){return t.trim().split(/^|\s+/)}function st(t){return t.classList||new lt(t)}function lt(t){this._node=t,this._names=ft(t.getAttribute("class")||"")}function ht(t,n){for(var e=st(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var wt={};(t.event=null,"undefined"!=typeof document)&&("onmouseenter"in document.documentElement||(wt={mouseenter:"mouseover",mouseleave:"mouseout"}));function Mt(t,n,e){return t=Nt(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function Nt(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function At(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=x&&(x=m+1);!(b=y[x])&&++x=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=at);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof n?function(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}:function(t,n,e){return function(){this.style.setProperty(t,n,e)}})(t,n,null==e?"":e)):ct(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?function(t){return function(){delete this[t]}}:"function"==typeof n?function(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}:function(t,n){return function(){this[t]=n}})(t,n)):this.node()[t]},classed:function(t,n){var e=ft(t+"");if(arguments.length<2){for(var r=st(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Tt:At,null==e&&(e=!1),r=0;r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):(n=rn.exec(t))?dn(parseInt(n[1],16)):(n=on.exec(t))?new yn(n[1],n[2],n[3],1):(n=an.exec(t))?new yn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=un.exec(t))?pn(n[1],n[2],n[3],n[4]):(n=cn.exec(t))?pn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=fn.exec(t))?bn(n[1],n[2]/100,n[3]/100,1):(n=sn.exec(t))?bn(n[1],n[2]/100,n[3]/100,n[4]):ln.hasOwnProperty(t)?dn(ln[t]):"transparent"===t?new yn(NaN,NaN,NaN,0):null}function dn(t){return new yn(t>>16&255,t>>8&255,255&t,1)}function pn(t,n,e,r){return r<=0&&(t=n=e=NaN),new yn(t,n,e,r)}function vn(t){return t instanceof Jt||(t=hn(t)),t?new yn((t=t.rgb()).r,t.g,t.b,t.opacity):new yn}function gn(t,n,e,r){return 1===arguments.length?vn(t):new yn(t,n,e,null==r?1:r)}function yn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function _n(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function bn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new xn(t,n,e,r)}function mn(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof xn)return new xn(t.h,t.s,t.l,t.opacity);if(t instanceof Jt||(t=hn(t)),!t)return new xn;if(t instanceof xn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new xn(a,u,c,t.opacity)}(t):new xn(t,n,e,null==r?1:r)}function xn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function wn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}Zt(Jt,hn,{displayable:function(){return this.rgb().displayable()},hex:function(){return this.rgb().hex()},toString:function(){return this.rgb()+""}}),Zt(yn,gn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new yn(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new yn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:function(){return"#"+_n(this.r)+_n(this.g)+_n(this.b)},toString:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}})),Zt(xn,mn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new xn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new xn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new yn(wn(t>=240?t-240:t+120,i,r),wn(t,i,r),wn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Mn=Math.PI/180,Nn=180/Math.PI,An=.96422,Tn=1,Sn=.82521,kn=4/29,En=6/29,Cn=3*En*En,Pn=En*En*En;function zn(t){if(t instanceof Dn)return new Dn(t.l,t.a,t.b,t.opacity);if(t instanceof Fn)return In(t);t instanceof yn||(t=vn(t));var n,e,r=On(t.r),i=On(t.g),o=On(t.b),a=qn((.2225045*r+.7168786*i+.0606169*o)/Tn);return r===i&&i===o?n=e=a:(n=qn((.4360747*r+.3850649*i+.1430804*o)/An),e=qn((.0139322*r+.0971045*i+.7141733*o)/Sn)),new Dn(116*a-16,500*(n-a),200*(a-e),t.opacity)}function Rn(t,n,e,r){return 1===arguments.length?zn(t):new Dn(t,n,e,null==r?1:r)}function Dn(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function qn(t){return t>Pn?Math.pow(t,1/3):t/Cn+kn}function Ln(t){return t>En?t*t*t:Cn*(t-kn)}function Un(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function On(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Bn(t){if(t instanceof Fn)return new Fn(t.h,t.c,t.l,t.opacity);if(t instanceof Dn||(t=zn(t)),0===t.a&&0===t.b)return new Fn(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r180||e<-180?e-360*Math.round(e/360):e):ee(isNaN(t)?n:t)}function oe(t){return 1==(t=+t)?ae:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):ee(isNaN(n)?e:n)}}function ae(t,n){var e=n-t;return e?re(t,e):ee(isNaN(t)?n:t)}Zt(Jn,Qn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Jn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Jn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Mn,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new yn(255*(n+e*(jn*r+Hn*i)),255*(n+e*(Xn*r+Gn*i)),255*(n+e*(Vn*r)),this.opacity)}}));var ue=function t(n){var e=oe(n);function r(t,n){var r=e((t=gn(t)).r,(n=gn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=ae(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function ce(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:de(e,r)})),o=ge.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:de(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:de(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:de(t,e)},{i:u-2,x:de(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(null,t),n=n._next;--Ve}function ur(){Je=(Qe=tr.now())+Ke,Ve=$e=0;try{ar()}finally{Ve=0,function(){var t,n,e=Xe,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Xe=n);Ge=t,fr(r)}(),Je=0}}function cr(){var t=tr.now(),n=t-Qe;n>Ze&&(Ke-=n,Qe=t)}function fr(t){Ve||($e&&($e=clearTimeout($e)),t-Je>24?(t<1/0&&($e=setTimeout(ur,t-tr.now()-Ke)),We&&(We=clearInterval(We))):(We||(Qe=tr.now(),We=setInterval(cr,Ze)),Ve=1,nr(ur)))}function sr(t,n,e){var r=new ir;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}ir.prototype=or.prototype={constructor:ir,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?er():+e)+(null==n?0:+n),this._next||Ge===this||(Ge?Ge._next=this:Xe=this,Ge=this),this._call=t,this._time=e,fr()},stop:function(){this._call&&(this._call=null,this._time=1/0,fr())}};var lr=I("start","end","cancel","interrupt"),hr=[],dr=0,pr=1,vr=2,gr=3,yr=4,_r=5,br=6;function mr(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(c){var f,s,l,h;if(e.state!==pr)return u();for(f in i)if((h=i[f]).name===e.name){if(h.state===gr)return sr(o);h.state===yr?(h.state=br,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fdr)throw new Error("too late; already scheduled");return e}function wr(t,n){var e=Mr(t,n);if(e.state>gr)throw new Error("too late; already running");return e}function Mr(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Nr(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>vr&&e.state<_r,e.state=br,e.timer.stop(),e.on.call(r?"interrupt":"cancel",t,t.__data__,e.index,e.group),delete o[i]):a=!1;a&&delete t.__transition}}function Ar(t,n,e){var r=t._id;return t.each(function(){var t=wr(this,r);(t.value||(t.value={}))[n]=e.apply(this,arguments)}),function(t){return Mr(t,r).value[n]}}function Tr(t,n){var e;return("number"==typeof n?de:n instanceof hn?ue:(e=hn(n))?(n=e,ue):ye)(t,n)}var Sr=Pt.prototype.constructor;function kr(t){return function(){this.style.removeProperty(t)}}var Er=0;function Cr(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function Pr(t){return Pt().transition(t)}function zr(){return++Er}var Rr=Pt.prototype;function Dr(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function qr(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}Cr.prototype=Pr.prototype={constructor:Cr,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=Q(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a=0&&(t=t.slice(0,n)),!t||"start"===t})}(n)?xr:wr;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=$(t),r="transform"===e?Ee:Tr;return this.attrTween(t,"function"==typeof n?(e.local?function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttributeNS(t.space,t.local))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttributeNS(t.space,t.local)}}:function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttribute(t))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttribute(t)}})(e,r,Ar(this,"attr."+t,n)):null==n?(e.local?function(t){return function(){this.removeAttributeNS(t.space,t.local)}}:function(t){return function(){this.removeAttribute(t)}})(e):(e.local?function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttributeNS(t.space,t.local);return a===o?null:a===r?i:i=n(r=a,e)}}:function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttribute(t);return a===o?null:a===r?i:i=n(r=a,e)}})(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=$(t);return this.tween(e,(r.local?function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttributeNS(t.space,t.local,n(e))}}(t,i)),e}return i._value=n,i}:function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttribute(t,n(e))}}(t,i)),e}return i._value=n,i})(r,n))},style:function(t,n,e){var r="transform"==(t+="")?ke:Tr;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=ct(this,t),a=(this.style.removeProperty(t),ct(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,kr(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=ct(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=ct(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Ar(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=wr(this,t),f=c.on,s=null==c.value[a]?o||(o=kr(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=ct(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n(r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Ar(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},remove:function(){return this.on("end.remove",(t=this._id,function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}));var t},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Mr(this.node(),e).tween,o=0,a=i.length;o0&&(r=o-p),M<0?h=d-v:M>0&&(a=u-v),x=gi,L.attr("cursor",wi.selection),B());break;default:return}pi()},!0).on("keyup.brush",function(){switch(t.event.keyCode){case 16:P&&(y=_=P=!1,B());break;case 18:x===_i&&(w<0?s=l:w>0&&(r=o),M<0?h=d:M>0&&(a=u),x=yi,B());break;case 32:x===gi&&(t.event.altKey?(w&&(s=l-p*w,r=o+p*w),M&&(h=d-v*M,a=u+v*M),x=_i):(w<0?s=l:w>0&&(r=o),M<0?h=d:M>0&&(a=u),x=yi),L.attr("cursor",wi[m]),B());break;default:return}pi()},!0).on("mousemove.brush",O,!0).on("mouseup.brush",Y,!0);It(t.event.view)}di(),Nr(b),c.call(b),D.start()}function O(){var t=Ot(b);!P||y||_||(Math.abs(t[0]-R[0])>Math.abs(t[1]-R[1])?_=!0:y=!0),R=t,g=!0,pi(),B()}function B(){var t;switch(p=R[0]-z[0],v=R[1]-z[1],x){case gi:case vi:w&&(p=Math.max(S-r,Math.min(E-s,p)),o=r+p,l=s+p),M&&(v=Math.max(k-a,Math.min(C-h,v)),u=a+v,d=h+v);break;case yi:w<0?(p=Math.max(S-r,Math.min(E-r,p)),o=r+p,l=s):w>0&&(p=Math.max(S-s,Math.min(E-s,p)),o=r,l=s+p),M<0?(v=Math.max(k-a,Math.min(C-a,v)),u=a+v,d=h):M>0&&(v=Math.max(k-h,Math.min(C-h,v)),u=a,d=h+v);break;case _i:w&&(o=Math.max(S,Math.min(E,r-p*w)),l=Math.max(S,Math.min(E,s+p*w))),M&&(u=Math.max(k,Math.min(C,a-v*M)),d=Math.max(k,Math.min(C,h+v*M)))}l1e-6)if(Math.abs(s*u-c*f)>1e-6&&i){var h=e-o,d=r-a,p=u*u+c*c,v=h*h+d*d,g=Math.sqrt(p),y=Math.sqrt(l),_=i*Math.tan((Fi-Math.acos((p+l-v)/(2*g*y)))/2),b=_/y,m=_/g;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*f)+","+(n+b*s)),this._+="A"+i+","+i+",0,0,"+ +(s*h>f*d)+","+(this._x1=t+m*u)+","+(this._y1=n+m*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+f:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-f)>1e-6)&&(this._+="L"+c+","+f),e&&(l<0&&(l=l%Ii+Ii),l>ji?this._+="A"+e+","+e+",0,1,"+s+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+s+","+(this._x1=c)+","+(this._y1=f):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Fi)+","+s+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};function Qi(){}function Ji(t,n){var e=new Qi;if(t instanceof Qi)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++ir!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function lo(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function ho(){}var po=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function vo(){var t=1,n=1,e=M,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(uo);else{var r=s(t),i=r[0],a=r[1];n=w(i,a,n),n=g(Math.floor(i/n)*n,Math.floor(a/n)*n,n)}return n.map(function(n){return o(t,n)})}function o(e,i){var o=[],u=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=e[0]>=r,po[f<<1].forEach(p);for(;++o=r,po[c|f<<1].forEach(p);po[f<<0].forEach(p);for(;++u=r,s=e[u*t]>=r,po[f<<1|s<<2].forEach(p);++o=r,l=s,s=e[u*t+o+1]>=r,po[c|f<<1|s<<2|l<<3].forEach(p);po[f|s<<3].forEach(p)}o=-1,s=e[u*t]>=r,po[s<<2].forEach(p);for(;++o=r,po[s<<2|l<<3].forEach(p);function p(t){var n,e,r=[t[0][0]+o,t[0][1]+u],c=[t[1][0]+o,t[1][1]+u],f=a(r),s=a(c);(n=d[f])?(e=h[s])?(delete d[n.end],delete h[e.start],n===e?(n.ring.push(c),i(n.ring)):h[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete d[n.end],n.ring.push(c),d[n.end=s]=n):(n=h[s])?(e=d[f])?(delete h[n.start],delete d[e.end],n===e?(n.ring.push(c),i(n.ring)):h[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete h[n.start],n.ring.unshift(r),h[n.start=f]=n):h[f]=d[s]={start:f,end:s,ring:[r,c]}}po[s<<3].forEach(p)}(e,i,function(t){r(t,e,i),function(t){for(var n=0,e=t.length,r=t[e-1][1]*t[0][0]-t[e-1][0]*t[0][1];++n0?o.push([t]):u.push(t)}),u.forEach(function(t){for(var n,e=0,r=o.length;e0&&a0&&u0&&o>0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?co(ao.call(t)):co(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:ho,i):r===u},i}function go(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[u-o+a*r]),n.data[u-e+a*r]=c/Math.min(u+1,r-1+o-u,o))}function yo(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[a+(u-o)*r]),n.data[a+(u-e)*r]=c/Math.min(u+1,i-1+o-u,o))}function _o(t){return t[0]}function bo(t){return t[1]}function mo(){return 1}var xo={},wo={},Mo=34,No=10,Ao=13;function To(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+"]"}).join(",")+"}")}function So(t){var n=Object.create(null),e=[];return t.forEach(function(t){for(var r in t)r in n||e.push(n[r]=r)}),e}function ko(t,n){var e=t+"",r=e.length;return r9999?"+"+ko(n,6):ko(n,4))+"-"+ko(t.getUTCMonth()+1,2)+"-"+ko(t.getUTCDate(),2)+(o?"T"+ko(e,2)+":"+ko(r,2)+":"+ko(i,2)+"."+ko(o,3)+"Z":i?"T"+ko(e,2)+":"+ko(r,2)+":"+ko(i,2)+"Z":r||e?"T"+ko(e,2)+":"+ko(r,2)+"Z":"")}function Co(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return wo;if(f)return f=!1,xo;var n,r,i=a;if(t.charCodeAt(i)===Mo){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===No?f=!0:r===Ao&&(f=!0,t.charCodeAt(a)===No&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;a=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function ia(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function oa(t){return t[0]}function aa(t){return t[1]}function ua(t,n,e){var r=new ca(null==n?oa:n,null==e?aa:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function ca(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function fa(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var sa=ua.prototype=ca.prototype;function la(t){return t.x+t.vx}function ha(t){return t.y+t.vy}function da(t){return t.index}function pa(t,n){var e=t.get(n);if(!e)throw new Error("missing: "+n);return e}function va(t){return t.x}function ga(t){return t.y}sa.copy=function(){var t,n,e=new ca(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=fa(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=fa(n));return e},sa.add=function(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return ra(this.cover(n,e),n,e,t)},sa.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=y)<<1|t>=g)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,v.data),b=n-+this._y.call(null,v.data),m=_*_+b*b;if(m=(u=(p+g)/2))?p=u:g=u,(s=a>=(c=(v+y)/2))?v=c:y=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},sa.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function ma(t){return(t=ba(Math.abs(t)))?t[1]:NaN}var xa,wa=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Ma(t){return new Na(t)}function Na(t){if(!(n=wa.exec(t)))throw new Error("invalid format: "+t);var n;this.fill=n[1]||" ",this.align=n[2]||">",this.sign=n[3]||"-",this.symbol=n[4]||"",this.zero=!!n[5],this.width=n[6]&&+n[6],this.comma=!!n[7],this.precision=n[8]&&+n[8].slice(1),this.trim=!!n[9],this.type=n[10]||""}function Aa(t,n){var e=ba(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Ma.prototype=Na.prototype,Na.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var Ta={"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Aa(100*t,n)},r:Aa,s:function(t,n){var e=ba(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(xa=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+ba(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function Sa(t){return t}var ka,Ea=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Ca(t){var n,e,r=t.grouping&&t.thousands?(n=t.grouping,e=t.thousands,function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}):Sa,i=t.currency,o=t.decimal,a=t.numerals?function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}}(t.numerals):Sa,u=t.percent||"%";function c(t){var n=(t=Ma(t)).fill,e=t.align,c=t.sign,f=t.symbol,s=t.zero,l=t.width,h=t.comma,d=t.precision,p=t.trim,v=t.type;"n"===v?(h=!0,v="g"):Ta[v]||(null==d&&(d=12),p=!0,v="g"),(s||"0"===n&&"="===e)&&(s=!0,n="0",e="=");var g="$"===f?i[0]:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",y="$"===f?i[1]:/[%p]/.test(v)?u:"",_=Ta[v],b=/[defgprs%]/.test(v);function m(t){var i,u,f,m=g,x=y;if("c"===v)x=_(t)+x,t="";else{var w=(t=+t)<0;if(t=_(Math.abs(t),d),p&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0){if(!+t[r])break t;i=0}}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),w&&0==+t&&(w=!1),m=(w?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=("s"===v?Ea[8+xa/3]:"")+x+(w&&"("===c?")":""),b)for(i=-1,u=t.length;++i(f=t.charCodeAt(i))||f>57){x=(46===f?o+t.slice(i+1):t.slice(i))+x,t=t.slice(0,i);break}}h&&!s&&(t=r(t,1/0));var M=m.length+t.length+x.length,N=M>1)+m+t+x+N.slice(M);break;default:t=N+m+t+x}return a(t)}return d=null==d?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,d)):Math.max(0,Math.min(20,d)),m.toString=function(){return t+""},m}return{format:c,formatPrefix:function(t,n){var e=c(((t=Ma(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(ma(n)/3))),i=Math.pow(10,-r),o=Ea[8+r/3];return function(t){return e(i*t)+o}}}}function Pa(n){return ka=Ca(n),t.format=ka.format,t.formatPrefix=ka.formatPrefix,ka}function za(t){return Math.max(0,-ma(Math.abs(t)))}function Ra(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(ma(n)/3)))-ma(Math.abs(t)))}function Da(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,ma(n)-ma(t))+1}function qa(){return new La}function La(){this.reset()}Pa({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),La.prototype={constructor:La,reset:function(){this.s=this.t=0},add:function(t){Oa(Ua,t,this.t),Oa(this,Ua.s,this.s),this.s?this.t+=Ua.t:this.s=Ua.t},valueOf:function(){return this.s}};var Ua=new La;function Oa(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}var Ba=1e-6,Ya=1e-12,Fa=Math.PI,Ia=Fa/2,ja=Fa/4,Ha=2*Fa,Xa=180/Fa,Ga=Fa/180,Va=Math.abs,$a=Math.atan,Wa=Math.atan2,Za=Math.cos,Qa=Math.ceil,Ja=Math.exp,Ka=Math.log,tu=Math.pow,nu=Math.sin,eu=Math.sign||function(t){return t>0?1:t<0?-1:0},ru=Math.sqrt,iu=Math.tan;function ou(t){return t>1?0:t<-1?Fa:Math.acos(t)}function au(t){return t>1?Ia:t<-1?-Ia:Math.asin(t)}function uu(t){return(t=nu(t/2))*t}function cu(){}function fu(t,n){t&&lu.hasOwnProperty(t.type)&&lu[t.type](t,n)}var su={Feature:function(t,n){fu(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=Za(n=(n*=Ga)/2+ja),a=nu(n),u=bu*a,c=_u*o+u*Za(i),f=u*r*nu(i);mu.add(Wa(f,c)),yu=t,_u=o,bu=a}function Su(t){return[Wa(t[1],t[0]),au(t[2])]}function ku(t){var n=t[0],e=t[1],r=Za(e);return[r*Za(n),r*nu(n),nu(e)]}function Eu(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Cu(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Pu(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function zu(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Ru(t){var n=ru(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var Du,qu,Lu,Uu,Ou,Bu,Yu,Fu,Iu,ju,Hu,Xu,Gu,Vu,$u,Wu,Zu,Qu,Ju,Ku,tc,nc,ec,rc,ic,oc,ac=qa(),uc={point:cc,lineStart:sc,lineEnd:lc,polygonStart:function(){uc.point=hc,uc.lineStart=dc,uc.lineEnd=pc,ac.reset(),wu.polygonStart()},polygonEnd:function(){wu.polygonEnd(),uc.point=cc,uc.lineStart=sc,uc.lineEnd=lc,mu<0?(Du=-(Lu=180),qu=-(Uu=90)):ac>Ba?Uu=90:ac<-Ba&&(qu=-90),ju[0]=Du,ju[1]=Lu},sphere:function(){Du=-(Lu=180),qu=-(Uu=90)}};function cc(t,n){Iu.push(ju=[Du=t,Lu=t]),nUu&&(Uu=n)}function fc(t,n){var e=ku([t*Ga,n*Ga]);if(Fu){var r=Cu(Fu,e),i=Cu([r[1],-r[0],0],r);Ru(i),i=Su(i);var o,a=t-Ou,u=a>0?1:-1,c=i[0]*Xa*u,f=Va(a)>180;f^(u*OuUu&&(Uu=o):f^(u*Ou<(c=(c+360)%360-180)&&cUu&&(Uu=n)),f?tvc(Du,Lu)&&(Lu=t):vc(t,Lu)>vc(Du,Lu)&&(Du=t):Lu>=Du?(tLu&&(Lu=t)):t>Ou?vc(Du,t)>vc(Du,Lu)&&(Lu=t):vc(t,Lu)>vc(Du,Lu)&&(Du=t)}else Iu.push(ju=[Du=t,Lu=t]);nUu&&(Uu=n),Fu=e,Ou=t}function sc(){uc.point=fc}function lc(){ju[0]=Du,ju[1]=Lu,uc.point=cc,Fu=null}function hc(t,n){if(Fu){var e=t-Ou;ac.add(Va(e)>180?e+(e>0?360:-360):e)}else Bu=t,Yu=n;wu.point(t,n),fc(t,n)}function dc(){wu.lineStart()}function pc(){hc(Bu,Yu),wu.lineEnd(),Va(ac)>Ba&&(Du=-(Lu=180)),ju[0]=Du,ju[1]=Lu,Fu=null}function vc(t,n){return(n-=t)<0?n+360:n}function gc(t,n){return t[0]-n[0]}function yc(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nFa?t+Math.round(-t/Ha)*Ha:t,n]}function zc(t,n,e){return(t%=Ha)?n||e?Cc(Dc(t),qc(n,e)):Dc(t):n||e?qc(n,e):Pc}function Rc(t){return function(n,e){return[(n+=t)>Fa?n-Ha:n<-Fa?n+Ha:n,e]}}function Dc(t){var n=Rc(t);return n.invert=Rc(-t),n}function qc(t,n){var e=Za(t),r=nu(t),i=Za(n),o=nu(n);function a(t,n){var a=Za(n),u=Za(t)*a,c=nu(t)*a,f=nu(n),s=f*e+u*r;return[Wa(c*i-s*o,u*e-f*r),au(s*i+c*o)]}return a.invert=function(t,n){var a=Za(n),u=Za(t)*a,c=nu(t)*a,f=nu(n),s=f*i-c*o;return[Wa(c*i+f*o,u*e+s*r),au(s*e-u*r)]},a}function Lc(t){function n(n){return(n=t(n[0]*Ga,n[1]*Ga))[0]*=Xa,n[1]*=Xa,n}return t=zc(t[0]*Ga,t[1]*Ga,t.length>2?t[2]*Ga:0),n.invert=function(n){return(n=t.invert(n[0]*Ga,n[1]*Ga))[0]*=Xa,n[1]*=Xa,n},n}function Uc(t,n,e,r,i,o){if(e){var a=Za(n),u=nu(n),c=r*e;null==i?(i=n+r*Ha,o=n-c/2):(i=Oc(a,i),o=Oc(a,o),(r>0?io)&&(i+=r*Ha));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function Yc(t,n){return Va(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function jc(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,A=N*M,T=A>Fa,S=v*x;if(Hc.add(Wa(S*N*nu(A),g*w+S*Za(A))),a+=T?M+N*Ha:M,T^d>=e^b>=e){var k=Cu(ku(h),ku(_));Ru(k);var E=Cu(o,k);Ru(E);var C=(T^M>=0?-1:1)*au(E[2]);(r>C||r===C&&(k[0]||k[1]))&&(u+=T^M>=0?1:-1)}}return(a<-Ba||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter($c))}return h}}function $c(t){return t.length>1}function Wc(t,n){return((t=t.x)[0]<0?t[1]-Ia-Ba:Ia-t[1])-((n=n.x)[0]<0?n[1]-Ia-Ba:Ia-n[1])}var Zc=Vc(function(){return!0},function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?Fa:-Fa,c=Va(o-e);Va(c-Fa)0?Ia:-Ia),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=Fa&&(Va(e-i)Ba?$a((nu(n)*(o=Za(r))*nu(e)-nu(r)*(i=Za(n))*nu(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}},function(t,n,e,r){var i;if(null==t)i=e*Ia,r.point(-Fa,i),r.point(0,i),r.point(Fa,i),r.point(Fa,0),r.point(Fa,-i),r.point(0,-i),r.point(-Fa,-i),r.point(-Fa,0),r.point(-Fa,i);else if(Va(t[0]-n[0])>Ba){var o=t[0]0,i=Va(n)>Ba;function o(t,e){return Za(t)*Za(e)>n}function a(t,e,r){var i=[1,0,0],o=Cu(ku(t),ku(e)),a=Eu(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=Cu(i,o),h=zu(i,f);Pu(h,zu(o,s));var d=l,p=Eu(h,d),v=Eu(d,d),g=p*p-v*(Eu(h,h)-1);if(!(g<0)){var y=ru(g),_=zu(d,(-p-y)/v);if(Pu(_,h),_=Su(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(Va(_[0]-m)Fa^(m<=_[0]&&_[0]<=x)){var T=zu(d,(-p+y)/v);return Pu(T,h),[_,Su(T)]}}}function u(n,e){var i=r?t:Fa-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return Vc(o,function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],v=o(l,h),g=r?v?0:u(l,h):v?u(l+(l<0?Fa:-Fa),h):0;if(!n&&(f=c=v)&&t.lineStart(),v!==c&&(!(d=a(n,p))||Yc(n,d)||Yc(p,d))&&(p[0]+=Ba,p[1]+=Ba,v=o(p[0],p[1])),v!==c)s=0,v?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1]),t.lineEnd()),n=d;else if(i&&n&&r^v){var y;g&e||!(y=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||n&&Yc(n,p)||t.point(p[0],p[1]),n=p,c=v,e=g},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}},function(n,r,i,o){Uc(o,t,e,i,n,r)},r?[0,-t]:[-Fa,t-Fa])}var Jc=1e9,Kc=-Jc;function tf(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return Va(r[0]-t)0?0:3:Va(r[0]-e)0?2:1:Va(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,v,g,y,_,b=a,m=Bc(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);y=!0,g=!1,p=v=NaN},lineEnd:function(){c&&(M(l,h),d&&g&&m.rejoin(),c.push(m.result()));x.point=w,g&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=T(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&Ic(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),y)l=o,h=a,d=u,y=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&g)b.point(o,a);else{var c=[p=Math.max(Kc,Math.min(Jc,p)),v=Math.max(Kc,Math.min(Jc,v))],m=[o=Math.max(Kc,Math.min(Jc,o)),a=Math.max(Kc,Math.min(Jc,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(g||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,v=a,g=u}return x}}var nf,ef,rf,of=qa(),af={sphere:cu,point:cu,lineStart:function(){af.point=cf,af.lineEnd=uf},lineEnd:cu,polygonStart:cu,polygonEnd:cu};function uf(){af.point=af.lineEnd=cu}function cf(t,n){nf=t*=Ga,ef=nu(n*=Ga),rf=Za(n),af.point=ff}function ff(t,n){t*=Ga;var e=nu(n*=Ga),r=Za(n),i=Va(t-nf),o=Za(i),a=r*nu(i),u=rf*e-ef*r*o,c=ef*e+rf*r*o;of.add(Wa(ru(a*a+u*u),c)),nf=t,ef=e,rf=r}function sf(t){return of.reset(),pu(t,af),+of}var lf=[null,null],hf={type:"LineString",coordinates:lf};function df(t,n){return lf[0]=t,lf[1]=n,sf(hf)}var pf={Feature:function(t,n){return gf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r0&&(i=df(t[o],t[o-1]))>0&&e<=i&&r<=i&&(e+r-i)*(1-Math.pow((e-r)/i,2))Ba}).map(c)).concat(g(Qa(o/d)*d,i,d).filter(function(t){return Va(t%v)>Ba}).map(f))}return _.lines=function(){return b().map(function(t){return{type:"LineString",coordinates:t}})},_.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},_.extent=function(t){return arguments.length?_.extentMajor(t).extentMinor(t):_.extentMinor()},_.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),_.precision(y)):[[r,u],[e,a]]},_.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),_.precision(y)):[[n,o],[t,i]]},_.step=function(t){return arguments.length?_.stepMajor(t).stepMinor(t):_.stepMinor()},_.stepMajor=function(t){return arguments.length?(p=+t[0],v=+t[1],_):[p,v]},_.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],_):[h,d]},_.precision=function(h){return arguments.length?(y=+h,c=wf(o,i,90),f=Mf(n,t,y),s=wf(u,a,90),l=Mf(r,e,y),_):y},_.extentMajor([[-180,-90+Ba],[180,90-Ba]]).extentMinor([[-180,-80-Ba],[180,80+Ba]])}function Af(t){return t}var Tf,Sf,kf,Ef,Cf=qa(),Pf=qa(),zf={point:cu,lineStart:cu,lineEnd:cu,polygonStart:function(){zf.lineStart=Rf,zf.lineEnd=Lf},polygonEnd:function(){zf.lineStart=zf.lineEnd=zf.point=cu,Cf.add(Va(Pf)),Pf.reset()},result:function(){var t=Cf/2;return Cf.reset(),t}};function Rf(){zf.point=Df}function Df(t,n){zf.point=qf,Tf=kf=t,Sf=Ef=n}function qf(t,n){Pf.add(Ef*t-kf*n),kf=t,Ef=n}function Lf(){qf(Tf,Sf)}var Uf=1/0,Of=Uf,Bf=-Uf,Yf=Bf,Ff={point:function(t,n){tBf&&(Bf=t);nYf&&(Yf=n)},lineStart:cu,lineEnd:cu,polygonStart:cu,polygonEnd:cu,result:function(){var t=[[Uf,Of],[Bf,Yf]];return Bf=Yf=-(Of=Uf=1/0),t}};var If,jf,Hf,Xf,Gf=0,Vf=0,$f=0,Wf=0,Zf=0,Qf=0,Jf=0,Kf=0,ts=0,ns={point:es,lineStart:rs,lineEnd:as,polygonStart:function(){ns.lineStart=us,ns.lineEnd=cs},polygonEnd:function(){ns.point=es,ns.lineStart=rs,ns.lineEnd=as},result:function(){var t=ts?[Jf/ts,Kf/ts]:Qf?[Wf/Qf,Zf/Qf]:$f?[Gf/$f,Vf/$f]:[NaN,NaN];return Gf=Vf=$f=Wf=Zf=Qf=Jf=Kf=ts=0,t}};function es(t,n){Gf+=t,Vf+=n,++$f}function rs(){ns.point=is}function is(t,n){ns.point=os,es(Hf=t,Xf=n)}function os(t,n){var e=t-Hf,r=n-Xf,i=ru(e*e+r*r);Wf+=i*(Hf+t)/2,Zf+=i*(Xf+n)/2,Qf+=i,es(Hf=t,Xf=n)}function as(){ns.point=es}function us(){ns.point=fs}function cs(){ss(If,jf)}function fs(t,n){ns.point=ss,es(If=Hf=t,jf=Xf=n)}function ss(t,n){var e=t-Hf,r=n-Xf,i=ru(e*e+r*r);Wf+=i*(Hf+t)/2,Zf+=i*(Xf+n)/2,Qf+=i,Jf+=(i=Xf*t-Hf*n)*(Hf+t),Kf+=i*(Xf+n),ts+=3*i,es(Hf=t,Xf=n)}function ls(t){this._context=t}ls.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,Ha)}},result:cu};var hs,ds,ps,vs,gs,ys=qa(),_s={point:cu,lineStart:function(){_s.point=bs},lineEnd:function(){hs&&ms(ds,ps),_s.point=cu},polygonStart:function(){hs=!0},polygonEnd:function(){hs=null},result:function(){var t=+ys;return ys.reset(),t}};function bs(t,n){_s.point=ms,ds=vs=t,ps=gs=n}function ms(t,n){vs-=t,gs-=n,ys.add(ru(vs*vs+gs*gs)),vs=t,gs=n}function xs(){this._string=[]}function ws(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function Ms(t){return function(n){var e=new Ns;for(var r in t)e[r]=t[r];return e.stream=n,e}}function Ns(){}function As(t,n,e){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),pu(e,t.stream(Ff)),n(Ff.result()),null!=r&&t.clipExtent(r),t}function Ts(t,n,e){return As(t,function(e){var r=n[1][0]-n[0][0],i=n[1][1]-n[0][1],o=Math.min(r/(e[1][0]-e[0][0]),i/(e[1][1]-e[0][1])),a=+n[0][0]+(r-o*(e[1][0]+e[0][0]))/2,u=+n[0][1]+(i-o*(e[1][1]+e[0][1]))/2;t.scale(150*o).translate([a,u])},e)}function Ss(t,n,e){return Ts(t,[[0,0],n],e)}function ks(t,n,e){return As(t,function(e){var r=+n,i=r/(e[1][0]-e[0][0]),o=(r-i*(e[1][0]+e[0][0]))/2,a=-i*e[0][1];t.scale(150*i).translate([o,a])},e)}function Es(t,n,e){return As(t,function(e){var r=+n,i=r/(e[1][1]-e[0][1]),o=-i*e[0][0],a=(r-i*(e[1][1]+e[0][1]))/2;t.scale(150*i).translate([o,a])},e)}xs.prototype={_radius:4.5,_circle:ws(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:null==this._circle&&(this._circle=ws(this._radius)),this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},Ns.prototype={constructor:Ns,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Cs=16,Ps=Za(30*Ga);function zs(t,n){return+n?function(t,n){function e(r,i,o,a,u,c,f,s,l,h,d,p,v,g){var y=f-r,_=s-i,b=y*y+_*_;if(b>4*n&&v--){var m=a+h,x=u+d,w=c+p,M=ru(m*m+x*x+w*w),N=au(w/=M),A=Va(Va(w)-1)n||Va((y*E+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*Ga:0,S()):[g*Xa,y*Xa,_*Xa]},A.angle=function(t){return arguments.length?(b=t%360*Ga,S()):b*Xa},A.precision=function(t){return arguments.length?(a=zs(u,N=t*t),k()):ru(N)},A.fitExtent=function(t,n){return Ts(A,t,n)},A.fitSize=function(t,n){return Ss(A,t,n)},A.fitWidth=function(t,n){return ks(A,t,n)},A.fitHeight=function(t,n){return Es(A,t,n)},function(){return n=t.apply(this,arguments),A.invert=n.invert&&T,S()}}function Us(t){var n=0,e=Fa/3,r=Ls(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*Ga,e=t[1]*Ga):[n*Xa,e*Xa]},i}function Os(t,n){var e=nu(t),r=(e+nu(n))/2;if(Va(r)0?n<-Ia+Ba&&(n=-Ia+Ba):n>Ia-Ba&&(n=Ia-Ba);var e=i/tu(Vs(n),r);return[e*nu(r*t),i-e*Za(r*t)]}return o.invert=function(t,n){var e=i-n,o=eu(r)*ru(t*t+e*e);return[Wa(t,Va(e))/r*eu(e),2*$a(tu(i/o,1/r))-Ia]},o}function Ws(t,n){return[t,n]}function Zs(t,n){var e=Za(t),r=t===n?nu(t):(e-Za(n))/(n-t),i=e/r+t;if(Va(r)=0;)n+=e[r].value;else n=1;t.value=n}function dl(t,n){var e,r,i,o,a,u=new yl(t),c=+t.value&&(u.value=t.value),f=[u];for(null==n&&(n=pl);e=f.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(a=i.length))for(e.children=new Array(a),o=a-1;o>=0;--o)f.push(r=e.children[o]=new yl(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(gl)}function pl(t){return t.children}function vl(t){t.data=t.data.data}function gl(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function yl(t){this.data=t,this.depth=this.height=0,this.parent=null}el.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(Qs+Js*i+o*(Ks+tl*i))-n)/(Qs+3*Js*i+o*(7*Ks+9*tl*i)))*r)*i*i,!(Va(e)Ba&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},al.invert=Is(au),ul.invert=Is(function(t){return 2*$a(t)}),cl.invert=function(t,n){return[-n,2*$a(Ja(t))-Ia]},yl.prototype=dl.prototype={constructor:yl,count:function(){return this.eachAfter(hl)},each:function(t){var n,e,r,i,o=this,a=[o];do{for(n=a.reverse(),a=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},sum:function(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})},sort:function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){var t=[];return this.each(function(n){t.push(n)}),t},leaves:function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},links:function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n},copy:function(){return dl(this).eachBefore(vl)}};var _l=Array.prototype.slice;function bl(t){for(var n,e,r=0,i=(t=function(t){for(var n,e,r=t.length;r;)e=Math.random()*r--|0,n=t[r],t[r]=t[e],t[e]=n;return t}(_l.call(t))).length,o=[];r0&&e*e>r*r+i*i}function Ml(t,n){for(var e=0;e(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function kl(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function El(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function Cl(t){this._=t,this.next=null,this.previous=null}function Pl(t){if(!(i=t.length))return 0;var n,e,r,i,o,a,u,c,f,s,l;if((n=t[0]).x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Sl(e,n,r=t[2]),n=new Cl(n),e=new Cl(e),r=new Cl(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(u=3;uh&&(h=u),g=s*s*v,(d=Math.max(h/g,g/l))>p){s-=u;break}p=d}y.push(a={value:s,dice:c1?n:1)},e}(Kl);var eh=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Kl);function rh(t,n){return t[0]-n[0]||t[1]-n[1]}function ih(t){for(var n,e,r,i=t.length,o=[0,1],a=2,u=2;u1&&(n=t[o[a-2]],e=t[o[a-1]],r=t[u],(e[0]-n[0])*(r[1]-n[1])-(e[1]-n[1])*(r[0]-n[0])<=0);)--a;o[a++]=u}return o.slice(0,a)}function oh(){return Math.random()}var ah=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(oh),uh=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(oh),ch=function t(n){function e(){var t=uh.source(n).apply(this,arguments);return function(){return Math.exp(t())}}return e.source=t,e}(oh),fh=function t(n){function e(t){return function(){for(var e=0,r=0;rr&&(n=e,e=r,r=n),function(t){return Math.max(e,Math.min(r,t))}}function Ah(t,n,e){var r=t[0],i=t[1],o=n[0],a=n[1];return i2?Th:Ah,i=o=null,l}function l(n){return isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),de)))(e)))},l.domain=function(t){return arguments.length?(a=vh.call(t,mh),f===wh||(f=Nh(a)),s()):a.slice()},l.range=function(t){return arguments.length?(u=gh.call(t),s()):u.slice()},l.rangeRound=function(t){return u=gh.call(t),c=be,s()},l.clamp=function(t){return arguments.length?(f=t?Nh(a):wh,l):f!==wh},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Eh(t,n){return kh()(t,n)}function Ch(n,e,r,i){var o,a=w(n,e,r);switch((i=Ma(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=Ra(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Da(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=za(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Ph(t){var n=t.domain;return t.ticks=function(t){var e=n();return m(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Ch(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i=n(),o=0,a=i.length-1,u=i[o],c=i[a];return c0?r=x(u=Math.floor(u/r)*r,c=Math.ceil(c/r)*r,e):r<0&&(r=x(u=Math.ceil(u*r)/r,c=Math.floor(c*r)/r,e)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(c/r)*r,n(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(c*r)/r,n(i)),t},t}function zh(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a0){for(;hc)break;v.push(l)}}else for(;h=1;--s)if(!((l=f*s)c)break;v.push(l)}}else v=m(h,d,Math.min(d-h,p)).map(r);return n?v.reverse():v},i.tickFormat=function(n,o){if(null==o&&(o=10===a?".0e":","),"function"!=typeof o&&(o=t.format(o)),n===1/0)return o;null==n&&(n=10);var u=Math.max(1,a*n/i.ticks().length);return function(t){var n=t/r(Math.round(e(t)));return n*a0))return u;do{u.push(a=new Date(+e)),n(e,o),t(e)}while(a=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return $h.setTime(+n),Wh.setTime(+r),t($h),t(Wh),Math.floor(e($h,Wh))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}var Qh=Zh(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});Qh.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?Zh(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Qh:null};var Jh=Qh.range,Kh=6e4,td=6048e5,nd=Zh(function(t){t.setTime(t-t.getMilliseconds())},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),ed=nd.range,rd=Zh(function(t){t.setTime(t-t.getMilliseconds()-1e3*t.getSeconds())},function(t,n){t.setTime(+t+n*Kh)},function(t,n){return(n-t)/Kh},function(t){return t.getMinutes()}),id=rd.range,od=Zh(function(t){t.setTime(t-t.getMilliseconds()-1e3*t.getSeconds()-t.getMinutes()*Kh)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),ad=od.range,ud=Zh(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Kh)/864e5},function(t){return t.getDate()-1}),cd=ud.range;function fd(t){return Zh(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Kh)/td})}var sd=fd(0),ld=fd(1),hd=fd(2),dd=fd(3),pd=fd(4),vd=fd(5),gd=fd(6),yd=sd.range,_d=ld.range,bd=hd.range,md=dd.range,xd=pd.range,wd=vd.range,Md=gd.range,Nd=Zh(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),Ad=Nd.range,Td=Zh(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});Td.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zh(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var Sd=Td.range,kd=Zh(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Kh)},function(t,n){return(n-t)/Kh},function(t){return t.getUTCMinutes()}),Ed=kd.range,Cd=Zh(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),Pd=Cd.range,zd=Zh(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),Rd=zd.range;function Dd(t){return Zh(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/td})}var qd=Dd(0),Ld=Dd(1),Ud=Dd(2),Od=Dd(3),Bd=Dd(4),Yd=Dd(5),Fd=Dd(6),Id=qd.range,jd=Ld.range,Hd=Ud.range,Xd=Od.range,Gd=Bd.range,Vd=Yd.range,$d=Fd.range,Wd=Zh(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),Zd=Wd.range,Qd=Zh(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Qd.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zh(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Jd=Qd.range;function Kd(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function tp(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function np(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function ep(t){var n=t.dateTime,e=t.date,r=t.time,i=t.periods,o=t.days,a=t.shortDays,u=t.months,c=t.shortMonths,f=sp(i),s=lp(i),l=sp(o),h=lp(o),d=sp(a),p=lp(a),v=sp(u),g=lp(u),y=sp(c),_=lp(c),b={a:function(t){return a[t.getDay()]},A:function(t){return o[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return u[t.getMonth()]},c:null,d:Pp,e:Pp,f:Lp,H:zp,I:Rp,j:Dp,L:qp,m:Up,M:Op,p:function(t){return i[+(t.getHours()>=12)]},Q:hv,s:dv,S:Bp,u:Yp,U:Fp,V:Ip,w:jp,W:Hp,x:null,X:null,y:Xp,Y:Gp,Z:Vp,"%":lv},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:$p,e:$p,f:Kp,H:Wp,I:Zp,j:Qp,L:Jp,m:tv,M:nv,p:function(t){return i[+(t.getUTCHours()>=12)]},Q:hv,s:dv,S:ev,u:rv,U:iv,V:ov,w:av,W:uv,x:null,X:null,y:cv,Y:fv,Z:sv,"%":lv},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p[r[0].toLowerCase()],e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h[r[0].toLowerCase()],e+r[0].length):-1},b:function(t,n,e){var r=y.exec(n.slice(e));return r?(t.m=_[r[0].toLowerCase()],e+r[0].length):-1},B:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=g[r[0].toLowerCase()],e+r[0].length):-1},c:function(t,e,r){return N(t,n,e,r)},d:xp,e:xp,f:Sp,H:Mp,I:Mp,j:wp,L:Tp,m:mp,M:Np,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s[r[0].toLowerCase()],e+r[0].length):-1},Q:Ep,s:Cp,S:Ap,u:dp,U:pp,V:vp,w:hp,W:gp,x:function(t,n,r){return N(t,e,n,r)},X:function(t,n,e){return N(t,r,n,e)},y:_p,Y:yp,Z:bp,"%":kp};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=tp(np(o.y))).getUTCDay(),r=i>4||0===i?Ld.ceil(r):Ld(r),r=zd.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=n(np(o.y))).getDay(),r=i>4||0===i?ld.ceil(r):ld(r),r=ud.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?tp(np(o.y)).getUTCDay():n(np(o.y)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,tp(o)):n(o)}}function N(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in ip?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",Kd);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t,tp);return n.toString=function(){return t},n}}}var rp,ip={"-":"",_:" ",0:"0"},op=/^\s*\d+/,ap=/^%/,up=/[\\^$*+?|[\]().{}]/g;function cp(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function bp(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function mp(t,n,e){var r=op.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function xp(t,n,e){var r=op.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function wp(t,n,e){var r=op.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Mp(t,n,e){var r=op.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Np(t,n,e){var r=op.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Ap(t,n,e){var r=op.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function Tp(t,n,e){var r=op.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Sp(t,n,e){var r=op.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function kp(t,n,e){var r=ap.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Ep(t,n,e){var r=op.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function Cp(t,n,e){var r=op.exec(n.slice(e));return r?(t.Q=1e3*+r[0],e+r[0].length):-1}function Pp(t,n){return cp(t.getDate(),n,2)}function zp(t,n){return cp(t.getHours(),n,2)}function Rp(t,n){return cp(t.getHours()%12||12,n,2)}function Dp(t,n){return cp(1+ud.count(Td(t),t),n,3)}function qp(t,n){return cp(t.getMilliseconds(),n,3)}function Lp(t,n){return qp(t,n)+"000"}function Up(t,n){return cp(t.getMonth()+1,n,2)}function Op(t,n){return cp(t.getMinutes(),n,2)}function Bp(t,n){return cp(t.getSeconds(),n,2)}function Yp(t){var n=t.getDay();return 0===n?7:n}function Fp(t,n){return cp(sd.count(Td(t),t),n,2)}function Ip(t,n){var e=t.getDay();return t=e>=4||0===e?pd(t):pd.ceil(t),cp(pd.count(Td(t),t)+(4===Td(t).getDay()),n,2)}function jp(t){return t.getDay()}function Hp(t,n){return cp(ld.count(Td(t),t),n,2)}function Xp(t,n){return cp(t.getFullYear()%100,n,2)}function Gp(t,n){return cp(t.getFullYear()%1e4,n,4)}function Vp(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+cp(n/60|0,"0",2)+cp(n%60,"0",2)}function $p(t,n){return cp(t.getUTCDate(),n,2)}function Wp(t,n){return cp(t.getUTCHours(),n,2)}function Zp(t,n){return cp(t.getUTCHours()%12||12,n,2)}function Qp(t,n){return cp(1+zd.count(Qd(t),t),n,3)}function Jp(t,n){return cp(t.getUTCMilliseconds(),n,3)}function Kp(t,n){return Jp(t,n)+"000"}function tv(t,n){return cp(t.getUTCMonth()+1,n,2)}function nv(t,n){return cp(t.getUTCMinutes(),n,2)}function ev(t,n){return cp(t.getUTCSeconds(),n,2)}function rv(t){var n=t.getUTCDay();return 0===n?7:n}function iv(t,n){return cp(qd.count(Qd(t),t),n,2)}function ov(t,n){var e=t.getUTCDay();return t=e>=4||0===e?Bd(t):Bd.ceil(t),cp(Bd.count(Qd(t),t)+(4===Qd(t).getUTCDay()),n,2)}function av(t){return t.getUTCDay()}function uv(t,n){return cp(Ld.count(Qd(t),t),n,2)}function cv(t,n){return cp(t.getUTCFullYear()%100,n,2)}function fv(t,n){return cp(t.getUTCFullYear()%1e4,n,4)}function sv(){return"+0000"}function lv(){return"%"}function hv(t){return+t}function dv(t){return Math.floor(+t/1e3)}function pv(n){return rp=ep(n),t.timeFormat=rp.format,t.timeParse=rp.parse,t.utcFormat=rp.utcFormat,t.utcParse=rp.utcParse,rp}pv({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var vv=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ");var gv=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),yv=1e3,_v=60*yv,bv=60*_v,mv=24*bv,xv=7*mv,wv=30*mv,Mv=365*mv;function Nv(t){return new Date(t)}function Av(t){return t instanceof Date?+t:+new Date(+t)}function Tv(t,n,r,i,o,a,u,c,f){var s=Eh(wh,wh),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),v=f("%I:%M"),g=f("%I %p"),y=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y"),x=[[u,1,yv],[u,5,5*yv],[u,15,15*yv],[u,30,30*yv],[a,1,_v],[a,5,5*_v],[a,15,15*_v],[a,30,30*_v],[o,1,bv],[o,3,3*bv],[o,6,6*bv],[o,12,12*bv],[i,1,mv],[i,2,2*mv],[r,1,xv],[n,1,wv],[n,3,3*wv],[t,1,Mv]];function M(e){return(u(e)=1?ly:t<=-1?-ly:Math.asin(t)}function py(t){return t.innerRadius}function vy(t){return t.outerRadius}function gy(t){return t.startAngle}function yy(t){return t.endAngle}function _y(t){return t&&t.padAngle}function by(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/cy(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,v=r+l,g=(h+p)/2,y=(d+v)/2,_=p-h,b=v-d,m=_*_+b*b,x=i-o,w=h*v-p*d,M=(b<0?-1:1)*cy(oy(0,x*x*m-w*w)),N=(w*b-_*M)/m,A=(-w*_-b*M)/m,T=(w*b+_*M)/m,S=(-w*_+b*M)/m,k=N-g,E=A-y,C=T-g,P=S-y;return k*k+E*E>C*C+P*P&&(N=T,A=S),{cx:N,cy:A,x01:-s,y01:-l,x11:N*(i/x-1),y11:A*(i/x-1)}}function my(t){this._context=t}function xy(t){return new my(t)}function wy(t){return t[0]}function My(t){return t[1]}function Ny(){var t=wy,n=My,e=ny(!0),r=null,i=xy,o=null;function a(a){var u,c,f,s=a.length,l=!1;for(null==r&&(o=i(f=Xi())),u=0;u<=s;++u)!(u=s;--l)u.point(g[l],y[l]);u.lineEnd(),u.areaEnd()}v&&(g[f]=+t(h,f,c),y[f]=+e(h,f,c),u.point(n?+n(h,f,c):g[f],r?+r(h,f,c):y[f]))}if(d)return u=null,d+""||null}function f(){return Ny().defined(i).curve(a).context(o)}return c.x=function(e){return arguments.length?(t="function"==typeof e?e:ny(+e),n=null,c):t},c.x0=function(n){return arguments.length?(t="function"==typeof n?n:ny(+n),c):t},c.x1=function(t){return arguments.length?(n=null==t?null:"function"==typeof t?t:ny(+t),c):n},c.y=function(t){return arguments.length?(e="function"==typeof t?t:ny(+t),r=null,c):e},c.y0=function(t){return arguments.length?(e="function"==typeof t?t:ny(+t),c):e},c.y1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:ny(+t),c):r},c.lineX0=c.lineY0=function(){return f().x(t).y(e)},c.lineY1=function(){return f().x(t).y(r)},c.lineX1=function(){return f().x(n).y(e)},c.defined=function(t){return arguments.length?(i="function"==typeof t?t:ny(!!t),c):i},c.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),c):a},c.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),c):o},c}function Ty(t,n){return nt?1:n>=t?0:NaN}function Sy(t){return t}my.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var ky=Cy(xy);function Ey(t){this._curve=t}function Cy(t){function n(n){return new Ey(t(n))}return n._curve=t,n}function Py(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Cy(t)):n()._curve},t}function zy(){return Py(Ny().curve(ky))}function Ry(){var t=Ay().curve(ky),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Py(e())},delete t.lineX0,t.lineEndAngle=function(){return Py(r())},delete t.lineX1,t.lineInnerRadius=function(){return Py(i())},delete t.lineY0,t.lineOuterRadius=function(){return Py(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Cy(t)):n()._curve},t}function Dy(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}Ey.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var qy=Array.prototype.slice;function Ly(t){return t.source}function Uy(t){return t.target}function Oy(t){var n=Ly,e=Uy,r=wy,i=My,o=null;function a(){var a,u=qy.call(arguments),c=n.apply(this,u),f=e.apply(this,u);if(o||(o=a=Xi()),t(o,+r.apply(this,(u[0]=c,u)),+i.apply(this,u),+r.apply(this,(u[0]=f,u)),+i.apply(this,u)),a)return o=null,a+""||null}return a.source=function(t){return arguments.length?(n=t,a):n},a.target=function(t){return arguments.length?(e=t,a):e},a.x=function(t){return arguments.length?(r="function"==typeof t?t:ny(+t),a):r},a.y=function(t){return arguments.length?(i="function"==typeof t?t:ny(+t),a):i},a.context=function(t){return arguments.length?(o=null==t?null:t,a):o},a}function By(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n=(n+r)/2,e,n,i,r,i)}function Yy(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n,e=(e+i)/2,r,e,r,i)}function Fy(t,n,e,r,i){var o=Dy(n,e),a=Dy(n,e=(e+i)/2),u=Dy(r,e),c=Dy(r,i);t.moveTo(o[0],o[1]),t.bezierCurveTo(a[0],a[1],u[0],u[1],c[0],c[1])}var Iy={draw:function(t,n){var e=Math.sqrt(n/sy);t.moveTo(e,0),t.arc(0,0,e,0,hy)}},jy={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Hy=Math.sqrt(1/3),Xy=2*Hy,Gy={draw:function(t,n){var e=Math.sqrt(n/Xy),r=e*Hy;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Vy=Math.sin(sy/10)/Math.sin(7*sy/10),$y=Math.sin(hy/10)*Vy,Wy=-Math.cos(hy/10)*Vy,Zy={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=$y*e,i=Wy*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var a=hy*o/5,u=Math.cos(a),c=Math.sin(a);t.lineTo(c*e,-u*e),t.lineTo(u*r-c*i,c*r+u*i)}t.closePath()}},Qy={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Jy=Math.sqrt(3),Ky={draw:function(t,n){var e=-Math.sqrt(n/(3*Jy));t.moveTo(0,2*e),t.lineTo(-Jy*e,-e),t.lineTo(Jy*e,-e),t.closePath()}},t_=Math.sqrt(3)/2,n_=1/Math.sqrt(12),e_=3*(n_/2+1),r_={draw:function(t,n){var e=Math.sqrt(n/e_),r=e/2,i=e*n_,o=r,a=e*n_+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(-.5*r-t_*i,t_*r+-.5*i),t.lineTo(-.5*o-t_*a,t_*o+-.5*a),t.lineTo(-.5*u-t_*c,t_*u+-.5*c),t.lineTo(-.5*r+t_*i,-.5*i-t_*r),t.lineTo(-.5*o+t_*a,-.5*a-t_*o),t.lineTo(-.5*u+t_*c,-.5*c-t_*u),t.closePath()}},i_=[Iy,jy,Gy,Qy,Zy,Ky,r_];function o_(){}function a_(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function u_(t){this._context=t}function c_(t){this._context=t}function f_(t){this._context=t}function s_(t,n){this._basis=new u_(t),this._beta=n}u_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:a_(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:a_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},c_.prototype={areaStart:o_,areaEnd:o_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:a_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},f_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:a_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},s_.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var l_=function t(n){function e(t){return 1===n?new u_(t):new s_(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function h_(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function d_(t,n){this._context=t,this._k=(1-n)/6}d_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:h_(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:h_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var p_=function t(n){function e(t){return new d_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function v_(t,n){this._context=t,this._k=(1-n)/6}v_.prototype={areaStart:o_,areaEnd:o_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:h_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var g_=function t(n){function e(t){return new v_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function y_(t,n){this._context=t,this._k=(1-n)/6}y_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:h_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var __=function t(n){function e(t){return new y_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function b_(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>fy){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>fy){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function m_(t,n){this._context=t,this._alpha=n}m_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:b_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var x_=function t(n){function e(t){return n?new m_(t,n):new d_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function w_(t,n){this._context=t,this._alpha=n}w_.prototype={areaStart:o_,areaEnd:o_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:b_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var M_=function t(n){function e(t){return n?new w_(t,n):new v_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function N_(t,n){this._context=t,this._alpha=n}N_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:b_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var A_=function t(n){function e(t){return n?new N_(t,n):new y_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function T_(t){this._context=t}function S_(t){return t<0?-1:1}function k_(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(S_(o)+S_(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function E_(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function C_(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function P_(t){this._context=t}function z_(t){this._context=new R_(t)}function R_(t){this._context=t}function D_(t){this._context=t}function q_(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function B_(t,n){return t[n]}function Y_(t){var n=t.map(F_);return O_(t).sort(function(t,e){return n[t]-n[e]})}function F_(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function I_(t){var n=t.map(j_);return O_(t).sort(function(t,e){return n[t]-n[e]})}function j_(t){for(var n,e=0,r=-1,i=t.length;++r0)){if(o/=h,h<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=r-c,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>s&&(s=o)}else if(h>0){if(o0)){if(o/=d,d<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=i-f,d||!(o<0)){if(o/=d,d<0){if(o>l)return;o>s&&(s=o)}else if(d>0){if(o0||l<1)||(s>0&&(t[0]=[c+s*h,f+s*d]),l<1&&(t[1]=[c+l*h,f+l*d]),!0)}}}}}function eb(t,n,e,r,i){var o=t[1];if(o)return!0;var a,u,c=t[0],f=t.left,s=t.right,l=f[0],h=f[1],d=s[0],p=s[1],v=(l+d)/2,g=(h+p)/2;if(p===h){if(v=r)return;if(l>d){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>d){if(c){if(c[1]>=i)return}else c=[(e-u)/a,e];o=[(i-u)/a,i]}else{if(c){if(c[1]=r)return}else c=[n,a*n+u];o=[r,a*r+u]}else{if(c){if(c[0]=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}},V_.prototype={constructor:V_,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Q_(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)e===(r=e.U).L?(i=r.R)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(W_(this,e),e=(t=e).U),e.C=!1,r.C=!0,Z_(this,r)):(i=r.L)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&(Z_(this,e),e=(t=e).U),e.C=!1,r.C=!0,W_(this,r)),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,a=t.R;if(e=o?a?Q_(a):o:a,i?i.L===t?i.L=e:i.R=e:this._=e,o&&a?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==a?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=a,a.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((n=i.R).C&&(n.C=!1,i.C=!0,W_(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Z_(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,W_(this,i),t=this._;break}}else if((n=i.L).C&&(n.C=!1,i.C=!0,Z_(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,W_(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,Z_(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var ab,ub=[];function cb(){$_(this),this.x=this.y=this.arc=this.site=this.cy=null}function fb(t){var n=t.P,e=t.N;if(n&&e){var r=n.site,i=t.site,o=e.site;if(r!==o){var a=i[0],u=i[1],c=r[0]-a,f=r[1]-u,s=o[0]-a,l=o[1]-u,h=2*(c*l-f*s);if(!(h>=-Nb)){var d=c*c+f*f,p=s*s+l*l,v=(l*d-f*p)/h,g=(c*p-s*d)/h,y=ub.pop()||new cb;y.arc=t,y.site=i,y.x=v+a,y.y=(y.cy=g+u)+Math.sqrt(v*v+g*g),t.circle=y;for(var _=null,b=xb._;b;)if(y.yMb)u=u.L;else{if(!((i=o-_b(u,a))>Mb)){r>-Mb?(n=u.P,e=u):i>-Mb?(n=u,e=u.N):n=e=u;break}if(!u.R){n=u;break}u=u.R}!function(t){mb[t.index]={site:t,halfedges:[]}}(t);var c=db(t);if(bb.insert(n,c),n||e){if(n===e)return sb(n),e=db(n.site),bb.insert(c,e),c.edge=e.edge=J_(n.site,c.site),fb(n),void fb(e);if(e){sb(n),sb(e);var f=n.site,s=f[0],l=f[1],h=t[0]-s,d=t[1]-l,p=e.site,v=p[0]-s,g=p[1]-l,y=2*(h*g-d*v),_=h*h+d*d,b=v*v+g*g,m=[(g*_-d*b)/y+s,(h*b-v*_)/y+l];tb(e.edge,f,p,m),c.edge=J_(f,t,null,m),e.edge=J_(t,p,null,m),fb(n),fb(e)}else c.edge=J_(n.site,c.site)}}function yb(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var a=t.P;if(!a)return-1/0;var u=(e=a.site)[0],c=e[1],f=c-n;if(!f)return u;var s=u-r,l=1/o-1/f,h=s/f;return l?(-h+Math.sqrt(h*h-2*l*(s*s/(-2*f)-c+f/2+i-o/2)))/l+r:(r+u)/2}function _b(t,n){var e=t.N;if(e)return yb(e,n);var r=t.site;return r[1]===n?r[0]:1/0}var bb,mb,xb,wb,Mb=1e-6,Nb=1e-12;function Ab(t,n){return n[1]-t[1]||n[0]-t[0]}function Tb(t,n){var e,r,i,o=t.sort(Ab).pop();for(wb=[],mb=new Array(t.length),bb=new V_,xb=new V_;;)if(i=ab,o&&(!i||o[1]Mb||Math.abs(i[0][1]-i[1][1])>Mb)||delete wb[o]}(a,u,c,f),function(t,n,e,r){var i,o,a,u,c,f,s,l,h,d,p,v,g=mb.length,y=!0;for(i=0;iMb||Math.abs(v-h)>Mb)&&(c.splice(u,0,wb.push(K_(a,d,Math.abs(p-t)Mb?[t,Math.abs(l-t)Mb?[Math.abs(h-r)Mb?[e,Math.abs(l-e)Mb?[Math.abs(h-n)=u)return null;var c=t-i.site[0],f=n-i.site[1],s=c*c+f*f;do{i=o.cells[r=a],a=null,i.halfedges.forEach(function(e){var r=o.edges[e],u=r.left;if(u!==i.site&&u||(u=r.right)){var c=t-u[0],f=n-u[1],l=c*c+f*f;lr?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Pb.prototype=Eb.prototype,t.version="5.9.7",t.bisect=i,t.bisectRight=i,t.bisectLeft=o,t.ascending=n,t.bisector=e,t.cross=function(t,n,e){var r,i,o,u,c=t.length,f=n.length,s=new Array(c*f);for(null==e&&(e=a),r=o=0;rt?1:n>=t?0:NaN},t.deviation=f,t.extent=s,t.histogram=function(){var t=v,n=s,e=M;function r(r){var o,a,u=r.length,c=new Array(u);for(o=0;ol;)h.pop(),--d;var p,v=new Array(d+1);for(o=0;o<=d;++o)(p=v[o]=[]).x0=o>0?h[o-1]:s,p.x1=o=r.length)return null!=t&&e.sort(t),null!=n?n(e):e;for(var c,f,s,l=-1,h=e.length,d=r[i++],p=Ji(),v=a();++lr.length)return e;var a,u=i[o-1];return null!=n&&o>=r.length?a=e.entries():(a=[],e.each(function(n,e){a.push({key:e,values:t(n,o)})})),null!=u?a.sort(function(t,n){return u(t.key,n.key)}):a}(o(t,0,no,eo),0)},key:function(t){return r.push(t),e},sortKeys:function(t){return i[r.length-1]=t,e},sortValues:function(n){return t=n,e},rollup:function(t){return n=t,e}}},t.set=oo,t.map=Ji,t.keys=function(t){var n=[];for(var e in t)n.push(e);return n},t.values=function(t){var n=[];for(var e in t)n.push(t[e]);return n},t.entries=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},t.color=hn,t.rgb=gn,t.hsl=mn,t.lab=Rn,t.hcl=Yn,t.lch=function(t,n,e,r){return 1===arguments.length?Bn(t):new Fn(e,n,t,null==r?1:r)},t.gray=function(t,n){return new Dn(t,0,0,null==n?1:n)},t.cubehelix=Qn,t.contours=vo,t.contourDensity=function(){var t=_o,n=bo,e=mo,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=co(20);function l(r){var i=new Float32Array(c*f),l=new Float32Array(c*f);r.forEach(function(r,o,s){var l=+t(r,o,s)+u>>a,h=+n(r,o,s)+u>>a,d=+e(r,o,s);l>=0&&l=0&&h>a),yo({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),go({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),yo({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),go({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),yo({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a);var d=s(i);if(!Array.isArray(d)){var p=A(i);d=w(0,p,d),(d=g(0,Math.floor(p/d)*d,d)).shift()}return vo().thresholds(d).size([c,f])(i).map(h)}function h(t){return t.value*=Math.pow(2,-2*a),t.coordinates.forEach(d),t}function d(t){t.forEach(p)}function p(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function y(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,l}return l.x=function(n){return arguments.length?(t="function"==typeof n?n:co(+n),l):t},l.y=function(t){return arguments.length?(n="function"==typeof t?t:co(+t),l):n},l.weight=function(t){return arguments.length?(e="function"==typeof t?t:co(+t),l):e},l.size=function(t){if(!arguments.length)return[r,i];var n=Math.ceil(t[0]),e=Math.ceil(t[1]);if(!(n>=0||n>=0))throw new Error("invalid size");return r=n,i=e,y()},l.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),y()},l.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?co(ao.call(t)):co(t),l):s},l.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=Math.round((Math.sqrt(4*t*t+1)-1)/2),y()},l},t.dispatch=I,t.drag=function(){var n,e,r,i,o=Gt,a=Vt,u=$t,c=Wt,f={},s=I("start","drag","end"),l=0,h=0;function d(t){t.on("mousedown.drag",p).filter(c).on("touchstart.drag",y).on("touchmove.drag",_).on("touchend.drag touchcancel.drag",b).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function p(){if(!i&&o.apply(this,arguments)){var u=m("mouse",a.apply(this,arguments),Ot,this,arguments);u&&(zt(t.event.view).on("mousemove.drag",v,!0).on("mouseup.drag",g,!0),It(t.event.view),Yt(),r=!1,n=t.event.clientX,e=t.event.clientY,u("start"))}}function v(){if(Ft(),!r){var i=t.event.clientX-n,o=t.event.clientY-e;r=i*i+o*o>h}f.mouse("drag")}function g(){zt(t.event.view).on("mousemove.drag mouseup.drag",null),jt(t.event.view,r),Ft(),f.mouse("end")}function y(){if(o.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=a.apply(this,arguments),u=r.length;for(n=0;nc+d||if+d||ou.index){var p=c-a.x-a.vx,v=f-a.y-a.vy,g=p*p+v*v;gt.r&&(t.r=t[n].r)}function u(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r=a)){(t.data!==n||t.next)&&(0===s&&(d+=(s=ea())*s),0===l&&(d+=(l=ea())*l),d1?(null==e?u.remove(t):u.set(t,d(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=na(.1);function o(t){for(var i,o=0,a=n.length;ovc(r[0],r[1])&&(r[1]=i[1]),vc(i[0],r[1])>vc(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=vc(r[1],i[0]))>a&&(a=u,Du=i[0],Lu=r[1])}return Iu=ju=null,Du===1/0||qu===1/0?[[NaN,NaN],[NaN,NaN]]:[[Du,qu],[Lu,Uu]]},t.geoCentroid=function(t){Hu=Xu=Gu=Vu=$u=Wu=Zu=Qu=Ju=Ku=tc=0,pu(t,_c);var n=Ju,e=Ku,r=tc,i=n*n+e*e+r*r;return i=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++e2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=cl,t.geoRotation=Lc,t.geoStream=pu,t.geoTransform=function(t){return{stream:Ms(t)}},t.cluster=function(){var t=fl,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter(function(n){var e=n.children;e?(n.x=function(t){return t.reduce(sl,0)/t.length}(e),n.y=function(t){return 1+t.reduce(ll,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)});var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.hierarchy=dl,t.pack=function(){var t=null,n=1,e=1,r=Rl;function i(i){return i.x=n/2,i.y=e/2,t?i.eachBefore(Ll(t)).eachAfter(Ul(r,.5)).eachBefore(Ol(1)):i.eachBefore(Ll(ql)).eachAfter(Ul(Rl,1)).eachAfter(Ul(r,i.r/Math.min(n,e))).eachBefore(Ol(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=null==(e=n)?null:zl(e),i):t;var e},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:Dl(+t),i):r},i},t.packSiblings=function(t){return Pl(t),t},t.packEnclose=bl,t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Yl(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0)throw new Error("cycle");return o}return e.id=function(n){return arguments.length?(t=zl(n),e):t},e.parentId=function(t){return arguments.length?(n=zl(t),e):n},e},t.tree=function(){var t=Gl,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new Ql(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new Ql(r[i],i)),e.parent=n;return(a.parent=new Ql(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore(function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)});var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),v=e/(l.depth||1);i.eachBefore(function(t){t.x=(t.x+d)*p,t.y=t.depth*v})}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=$l(u),o=Vl(o),u&&o;)c=Vl(c),(a=$l(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Wl(Zl(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!$l(a)&&(a.t=u,a.m+=l-s),o&&!Vl(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=nh,n=!1,e=1,r=1,i=[0],o=Rl,a=Rl,u=Rl,c=Rl,f=Rl;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(Bl),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}for(var l=f[n],h=r/2+l,d=n+1,p=e-1;d>>1;f[v]c-o){var _=(i*y+a*g)/r;t(n,d,g,i,o,_,c),t(d,e,y,_,o,a,c)}else{var b=(o*y+c*g)/r;t(n,d,g,i,o,a,b),t(d,e,y,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Yl,t.treemapSlice=Jl,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Jl:Yl)(t,n,e,r,i)},t.treemapSquarify=nh,t.treemapResquarify=eh,t.interpolate=_e,t.interpolateArray=le,t.interpolateBasis=te,t.interpolateBasisClosed=ne,t.interpolateDate=he,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateHue=function(t,n){var e=ie(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateNumber=de,t.interpolateObject=pe,t.interpolateRound=be,t.interpolateString=ye,t.interpolateTransformCss=ke,t.interpolateTransformSvg=Ee,t.interpolateZoom=qe,t.interpolateRgb=ue,t.interpolateRgbBasis=fe,t.interpolateRgbBasisClosed=se,t.interpolateHsl=Ue,t.interpolateHslLong=Oe,t.interpolateLab=function(t,n){var e=ae((t=Rn(t)).l,(n=Rn(n)).l),r=ae(t.a,n.a),i=ae(t.b,n.b),o=ae(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateHcl=Ye,t.interpolateHclLong=Fe,t.interpolateCubehelix=je,t.interpolateCubehelixLong=He,t.piecewise=function(t,n){for(var e=0,r=n.length-1,i=n[0],o=new Array(r<0?0:r);e=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;nu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonLength=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],a=o[0],u=o[1],c=0;++r0?a[n-1]:r[0],n=o?[a[o-1],r]:[a[n-1],a[n]]},c.unknown=function(t){return arguments.length?(n=t,c):c},c.thresholds=function(){return a.slice()},c.copy=function(){return t().domain([e,r]).range(u).unknown(n)},hh.apply(Ph(c),arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],o=1;function a(t){return t<=t?r[i(e,t,0,o)]:n}return a.domain=function(t){return arguments.length?(e=gh.call(t),o=Math.min(e.length,r.length-1),a):e.slice()},a.range=function(t){return arguments.length?(r=gh.call(t),o=Math.min(e.length,r.length-1),a):r.slice()},a.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},a.unknown=function(t){return arguments.length?(n=t,a):n},a.copy=function(){return t().domain(e).range(r).unknown(n)},hh.apply(a,arguments)},t.scaleTime=function(){return hh.apply(Tv(Td,Nd,sd,ud,od,rd,nd,Qh,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return hh.apply(Tv(Qd,Wd,qd,zd,Cd,kd,nd,Qh,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scaleSequential=function t(){var n=Ph(Sv()(wh));return n.copy=function(){return kv(n,t())},dh.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=Bh(Sv()).domain([1,10]);return n.copy=function(){return kv(n,t()).base(n.base())},dh.apply(n,arguments)},t.scaleSequentialPow=Ev,t.scaleSequentialSqrt=function(){return Ev.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Ih(Sv());return n.copy=function(){return kv(n,t()).constant(n.constant())},dh.apply(n,arguments)},t.scaleSequentialQuantile=function t(){var e=[],r=wh;function o(t){if(!isNaN(t=+t))return r((i(e,t)-1)/(e.length-1))}return o.domain=function(t){if(!arguments.length)return e.slice();e=[];for(var r,i=0,a=t.length;i1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return Gg.h=360*t-100,Gg.s=1.5-1.5*n,Gg.l=.8-.9*n,Gg+""},t.interpolateWarm=Hg,t.interpolateCool=Xg,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,Vg.r=255*(n=Math.sin(t))*n,Vg.g=255*(n=Math.sin(t+$g))*n,Vg.b=255*(n=Math.sin(t+Wg))*n,Vg+""},t.interpolateViridis=Qg,t.interpolateMagma=Jg,t.interpolateInferno=Kg,t.interpolatePlasma=ty,t.create=function(t){return zt(W(t).call(document.documentElement))},t.creator=W,t.local=Dt,t.matcher=tt,t.mouse=Ot,t.namespace=$,t.namespaces=V,t.clientPoint=Ut,t.select=zt,t.selectAll=function(t){return"string"==typeof t?new Ct([document.querySelectorAll(t)],[document.documentElement]):new Ct([null==t?[]:t],Et)},t.selection=Pt,t.selector=Q,t.selectorAll=K,t.style=ct,t.touch=Bt,t.touches=function(t,n){null==n&&(n=Lt().touches);for(var e=0,r=n?n.length:0,i=new Array(r);ed;if(u||(u=c=Xi()),hfy)if(v>hy-fy)u.moveTo(h*iy(d),h*uy(d)),u.arc(0,0,h,d,p,!g),l>fy&&(u.moveTo(l*iy(p),l*uy(p)),u.arc(0,0,l,p,d,g));else{var y,_,b=d,m=p,x=d,w=p,M=v,N=v,A=a.apply(this,arguments)/2,T=A>fy&&(r?+r.apply(this,arguments):cy(l*l+h*h)),S=ay(ey(h-l)/2,+e.apply(this,arguments)),k=S,E=S;if(T>fy){var C=dy(T/l*uy(A)),P=dy(T/h*uy(A));(M-=2*C)>fy?(x+=C*=g?1:-1,w-=C):(M=0,x=w=(d+p)/2),(N-=2*P)>fy?(b+=P*=g?1:-1,m-=P):(N=0,b=m=(d+p)/2)}var z=h*iy(b),R=h*uy(b),D=l*iy(w),q=l*uy(w);if(S>fy){var L,U=h*iy(m),O=h*uy(m),B=l*iy(x),Y=l*uy(x);if(v1?0:s<-1?sy:Math.acos(s))/2),G=cy(L[0]*L[0]+L[1]*L[1]);k=ay(S,(l-G)/(X-1)),E=ay(S,(h-G)/(X+1))}}N>fy?E>fy?(y=by(B,Y,z,R,h,E,g),_=by(U,O,D,q,h,E,g),u.moveTo(y.cx+y.x01,y.cy+y.y01),Efy&&M>fy?k>fy?(y=by(D,q,U,O,l,-k,g),_=by(z,R,B,Y,l,-k,g),u.lineTo(y.cx+y.x01,y.cy+y.y01),k0&&(d+=l);for(null!=n?p.sort(function(t,e){return n(v[t],v[e])}):null!=e&&p.sort(function(t,n){return e(a[t],a[n])}),u=0,f=d?(y-h*b)/d:0;u0?l*f:0)+b,v[c]={data:a[c],index:u,value:l,startAngle:g,endAngle:s,padAngle:_};return v}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:ny(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:ny(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:ny(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:ny(+t),a):o},a},t.areaRadial=Ry,t.radialArea=Ry,t.lineRadial=zy,t.radialLine=zy,t.pointRadial=Dy,t.linkHorizontal=function(){return Oy(By)},t.linkVertical=function(){return Oy(Yy)},t.linkRadial=function(){var t=Oy(Fy);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.symbol=function(){var t=ny(Iy),n=ny(64),e=null;function r(){var r;if(e||(e=r=Xi()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),r)return e=null,r+""||null}return r.type=function(n){return arguments.length?(t="function"==typeof n?n:ny(n),r):t},r.size=function(t){return arguments.length?(n="function"==typeof t?t:ny(+t),r):n},r.context=function(t){return arguments.length?(e=null==t?null:t,r):e},r},t.symbols=i_,t.symbolCircle=Iy,t.symbolCross=jy,t.symbolDiamond=Gy,t.symbolSquare=Qy,t.symbolStar=Zy,t.symbolTriangle=Ky,t.symbolWye=r_,t.curveBasisClosed=function(t){return new c_(t)},t.curveBasisOpen=function(t){return new f_(t)},t.curveBasis=function(t){return new u_(t)},t.curveBundle=l_,t.curveCardinalClosed=g_,t.curveCardinalOpen=__,t.curveCardinal=p_,t.curveCatmullRomClosed=M_,t.curveCatmullRomOpen=A_,t.curveCatmullRom=x_,t.curveLinearClosed=function(t){return new T_(t)},t.curveLinear=xy,t.curveMonotoneX=function(t){return new P_(t)},t.curveMonotoneY=function(t){return new z_(t)},t.curveNatural=function(t){return new D_(t)},t.curveStep=function(t){return new L_(t,.5)},t.curveStepAfter=function(t){return new L_(t,1)},t.curveStepBefore=function(t){return new L_(t,0)},t.stack=function(){var t=ny([]),n=O_,e=U_,r=B_;function i(i){var o,a,u=t.apply(this,arguments),c=i.length,f=u.length,s=new Array(f);for(o=0;o0){for(var e,r,i,o=0,a=t[0].length;o0)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c=0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):r[0]=o},t.stackOffsetNone=U_,t.stackOffsetSilhouette=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;apr&&e.name===n)return new Cr([[t]],si,n,+r);return null},t.interrupt=Nr,t.voronoi=function(){var t=X_,n=G_,e=null;function r(r){return new Tb(r.map(function(e,i){var o=[Math.round(t(e,i,r)/Mb)*Mb,Math.round(n(e,i,r)/Mb)*Mb];return o.index=i,o.data=e,o}),e)}return r.polygons=function(t){return r(t).polygons()},r.links=function(t){return r(t).links()},r.triangles=function(t){return r(t).triangles()},r.x=function(n){return arguments.length?(t="function"==typeof n?n:H_(+n),r):t},r.y=function(t){return arguments.length?(n="function"==typeof t?t:H_(+t),r):n},r.extent=function(t){return arguments.length?(e=null==t?null:[[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]],r):e&&[[e[0][0],e[0][1]],[e[1][0],e[1][1]]]},r.size=function(t){return arguments.length?(e=null==t?null:[[0,0],[+t[0],+t[1]]],r):e&&[e[1][0]-e[0][0],e[1][1]-e[0][1]]},r},t.zoom=function(){var n,e,r=Db,i=qb,o=Bb,a=Ub,u=Ob,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=qe,h=[],d=I("start","zoom","end"),p=500,v=150,g=0;function y(t){t.property("__zoom",Lb).on("wheel.zoom",N).on("mousedown.zoom",A).on("dblclick.zoom",T).filter(u).on("touchstart.zoom",S).on("touchmove.zoom",k).on("touchend.zoom touchcancel.zoom",E).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new Eb(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new Eb(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e){t.on("start.zoom",function(){w(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){w(this,arguments).end()}).tween("zoom",function(){var t=arguments,r=w(this,t),o=i.apply(this,t),a=e||m(o),u=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),c=this.__zoom,f="function"==typeof n?n.apply(this,t):n,s=l(c.invert(a).concat(u/c.k),f.invert(a).concat(u/f.k));return function(t){if(1===t)t=f;else{var n=s(t),e=u/n[2];t=new Eb(e,a[0]-n[0]*e,a[1]-n[1]*e)}r.zoom(null,t)}})}function w(t,n){for(var e,r=0,i=h.length;rg}n.zoom("mouse",o(b(n.that.__zoom,n.mouse[0]=Ot(n.that),n.mouse[1]),n.extent,f))},!0).on("mouseup.zoom",function(){i.on("mousemove.zoom mouseup.zoom",null),jt(t.event.view,n.moved),Rb(),n.end()},!0),a=Ot(this),u=t.event.clientX,c=t.event.clientY;It(t.event.view),zb(),n.mouse=[a,this.__zoom.invert(a)],Nr(this),n.start()}}function T(){if(r.apply(this,arguments)){var n=this.__zoom,e=Ot(this),a=n.invert(e),u=n.k*(t.event.shiftKey?.5:2),c=o(b(_(n,u),e,a),i.apply(this,arguments),f);Rb(),s>0?zt(this).transition().duration(s).call(x,c,e):zt(this).call(y.transform,c)}}function S(){if(r.apply(this,arguments)){var e,i,o,a,u=w(this,arguments),c=t.event.changedTouches,f=c.length;for(zb(),i=0;i - + - + - + - + - + - - + + - + - + - + - + diff --git a/static/fonts/icomoon.ttf b/static/fonts/icomoon.ttf index ec25f2f3c..44a142a6a 100644 Binary files a/static/fonts/icomoon.ttf and b/static/fonts/icomoon.ttf differ diff --git a/static/fonts/icomoon.woff b/static/fonts/icomoon.woff index b68501db6..7802d41fc 100644 Binary files a/static/fonts/icomoon.woff and b/static/fonts/icomoon.woff differ diff --git a/static/graph.css b/static/graph.css new file mode 100644 index 000000000..10b036a9b --- /dev/null +++ b/static/graph.css @@ -0,0 +1,326 @@ +#logicalGraphParent{ + /* declaring custom variables used in css set via ts later from the graph config variables */ + --selectedBg : red; + --selectedConstructBg:red; + --nodeBorder : red; + --nodeBg:red; + --graphText:red; + --branchBg:red; + --constructBg:red; + --embeddedApp:red; + --constructIcon:red; + --edgeColor:red; + --commentEdgeColor:red; + --matchingEdgeColor:red; +} + +#logicalGraph { + display: none; + -webkit-transform-origin: 0 0; +} + +#selectionRectangle{ + border:1px black solid; + position: absolute; + display: none; +} + +#logicalGraph svg{ + position:absolute; + z-index: 2; + pointer-events: none; + width: 10000px; + height: 10000px; + transform: translate(-50%,-50%); +} + +#logicalGraph svg path { + stroke-width:4; + fill:transparent; + stroke:var(--edgeColor); + pointer-events: all; +} + +#logicalGraph svg path.commentEdge{ + stroke:var(--commentEdgeColor); + stroke-width: 2; +} + +#logicalGraph svg path.edgeClickTarget{ + stroke-width: 10px; + stroke:transparent; +} + +.container, .header{ + z-index: 3; + pointer-events: all; +} + +.node .container, .constructOutputApp, .constructInputApp{ + z-index: 5; + pointer-events: all; +} + +.node .container .body{ + z-index: 3; + pointer-events: all; +} + +.construct_node .container{ + z-index: 1; + max-width: none; +} + +.construct_node .container .basic_node .body{ + z-index: 3; + pointer-events: all; +} + +#portContainer { + position: relative; + z-index: 10; +} + +#portContainer i { + height: 100%; + width: 100%; + font-size: 14px; + transform: translate(-1px,-1.5px); + pointer-events: none; + color: #2e3192; + position: absolute; +} + +.node { + position: absolute; + height: 60px; + pointer-events: none; + width: 60px; +} + +.node .nodeContent { + width: 100%; + height: 100%; +} + +.node .container{ + position: relative; + height: 100%; + width: 100%; + padding:0px; + transform: translate(-50%,-50%); + pointer-events: none; + max-width: unset; +} + +.node .construct_node .basic_node .container{ + transform: none; +} + +.node .construct_node .basic_node .header-name{ + font-size: 16px; + font-weight: 400; +} + +.node .body { + position: relative; + background-color: var(--nodeBg); + border-radius: 50%; + border: 1.5px solid var(--nodeBorder); + width: 100%; + height: 100%; + box-shadow: -2px 5px 5px -4px #555555; + pointer-events: all; +} + +.node .comment_node .body { + border: none; +} + +.node .innerRing { + background-color: var(--nodeBg); + border-radius: 50%; + border: 1.5px solid var(--nodeBorder); + position: absolute; + inset: 6px 6px 6px 6px; + z-index: 2; +} + +.node .outerRing { + border-radius: 50%; + border: 1.5px solid var(--nodeBorder); + position: absolute; + background-color: var(--branchBg); + inset: -7px -7px -7px -7px; + box-shadow: -2px 5px 5px -4px #555555; +} + +.node .body span, .node .construct_node .basic_node span{ + width: 100%; + height: 100%; + font-size: 75px; /*this is the same as the circle size of the node givign that information to the icon in ::before */ +} + +.node .branch_node .body { + border-radius: 50%; + border: none !important; + width: 100%; + height: 100%; +} + +.node .construct_node .body { + background-color: var(--constructBg); +} + +.node .construct_node .basic_node .body { + background-color: var(--embeddedApp); +} + +.node .construct_node span { + font-size: 150px; + color: var(--constructIcon); +} + +.node .construct_node .constructInputApp, .node .construct_node .constructOutputApp{ + position: absolute; + transform: translate(-50%, -50%); +} + +.node .branch_node .body span{ + font-size: 100px; /*this is the same as the circle size of the node givign that information to the icon in ::before */ +} + +.node .body span::before{ + font-size: .35em; /* em is like using % for font sizes. it is a % relative to the font-size of the parent, in this case the span itself */ + top: 50%; + left: 50%; + position: absolute; + transform: translate(-50%,-50%); + z-index: 5; + pointer-events: none; +} + +#portContainer .port{ + width: 12px; + height: 12px; + position: absolute; + border-radius: 50%; + background-color: red; + transform: translate(-50%,-50%); + top: 50%; + left: 100%; +} + +#portContainer div{ + position: relative; +} + +#portContainer div .portTitle{ + position: absolute; + font-size: 12px; + top: -1px; + width: 100px; + pointer-events: none; + display: none; + + /* white outer border, a bit ugle but the only way i found to do it */ + text-shadow: + -0.8px -0.8px 0 #ffffff, + 0 -0.8px 0 #ffffff, + 0.8px -0.8px 0 #ffffff, + 0.8px 0 0 #ffffff, + 0.8px 0.8px 0 #ffffff, + 0 0.8px 0 #ffffff, + -0.8px 0.8px 0 #ffffff, + -0.8px 0 0 #ffffff; +} + +#portContainer .selected div .portTitle{ + display: block; +} + +.node .container.selected .body { + outline: solid 2px var(--nodeBorder); + outline-offset: 0px; + background-color: var(--selectedBg); + /* box-shadow: 0 8px 6px -6px black; */ +} + +.node .construct_node .container.selected .body { + outline: solid 5px var(--nodeBorder); +} + +.node .construct_node .constructInputApp .container.selected .body, .node .construct_node .constructOutputApp .container.selected .body { + outline: solid 2px var(--nodeBorder); +} + +.node .branch_node .container.selected .outerRing { + outline: solid 2px var(--nodeBorder); + outline-offset: 0px; +} + +.node .branch_node .container.selected .innerRing { + background-color: var(--selectedBg) !important; +} + +.node .construct_node .container.selected .body { + background-color: var(--selectedConstructBg); +} + +.node .construct_node .basic_node .container.selected .body { + background-color: var(--selectedBg); +} + +#portContainer .port.match.selected { + outline: 2px solid var(--matchingEdgeColor); +} + +.node .header-name { + color:var(--graphText) +} + +.node .container.selected .header-name { + font-weight: 500; + color:var(--graphText) +} + +.node .header { + position: absolute; + overflow: hidden; + text-align: center; + bottom:calc(100% + 10px); + left: 50%; + transform: translateX(-50%); + max-width: 150px; +} + +.node .branch_node .header{ + bottom:calc(100% + 20px); +} + +.node .header .header-name, .node .body .contents .app-name { + padding: 4px; + user-select: none; + line-height: 14px; + + /* white outer border, a bit ugle but the only way i found to do it */ + text-shadow: + -1px -1px 0 #ffffff, + 0 -1px 0 #ffffff, + 1px -1px 0 #ffffff, + 1px 0 0 #ffffff, + 1px 1px 0 #ffffff, + 0 1px 0 #ffffff, + -1px 1px 0 #ffffff, + -1px 0 0 #ffffff; +} + +.construct_node .header-name{ + font-size: 20px; + font-weight: 650; +} + +.node.transition{ + transition: width .2s cubic-bezier(0.68, 2.15, 0.5, 0.75), height .2s cubic-bezier(0.68, 2.15, 0.5, 0.75); + +} diff --git a/static/svg.css b/static/svg.css index 5cb62cbb3..eb7e1d384 100644 --- a/static/svg.css +++ b/static/svg.css @@ -2,136 +2,3 @@ svg { width: 100%; height: 100%; } - -g.node rect { - stroke-width: 2; - rx: 4; - ry: 4; -} - -g.node rect.paletteNode { - fill: rgba(180,180,180,1); - stroke: grey; -} - -g.node rect.selected { - stroke: black; -} - -g.node rect.resize-control { - rx: 0; - ry: 0; - fill : none; - stroke: none; - cursor: nwse-resize; -} - -g.node text.resize-control-label { - fill: grey; -} - -g.node rect.shrink-button { - fill: rgba(180,180,180,1); - stroke: grey; -} - -g.node rect.collapse-button { - fill: rgba(180,180,180,1); - stroke: grey; -} - -g.node rect.expand-button { - fill: rgba(180,180,180,1); - stroke: grey; -} - -g.node text.header, g.node text.subheader { - text-anchor: middle; - font-family: sans-serif; - pointer-events: none; - user-select: none; -} - -g.node text.outputAppName { - text-anchor: end; -} - -g.node text.exitAppName { - text-anchor: end; -} - -g.node g circle { - stroke: white; - stroke-width: 2; - fill: grey; - cursor: crosshair; -} - -g.node g path { - stroke: white; - stroke-width: 2; - fill: grey; - cursor: crosshair; -} - -g.node g .hiddenPortIcon{ - display: none; -} - -g.node g.inputPorts.no-flip text, g.node g.outputPorts.flipped text, g.node g.exitPorts.flipped text { - text-anchor: start; - font-family: sans-serif; - pointer-events: none; - fill: #444444; - user-select: none; -} - -g.node g.inputPorts.no-flip text.middle, g.node g.outputPorts.flipped text.middle, g.node g.exitPorts.flipped text.middle { - text-anchor: middle; -} - -g.node g.inputLocalPorts.no-flip text, g.node g.outputLocalPorts.flipped text, g.node g.exitLocalPorts.flipped text { - text-anchor: start; - font-family: sans-serif; - pointer-events: none; - fill: grey; - user-select: none; -} - -g.node g.outputPorts.no-flip text, g.node g.exitPorts.no-flip text, g.node g.inputPorts.flipped text { - text-anchor: end; - font-family: sans-serif; - pointer-events: none; - fill: #444444; - user-select: none; -} - -g.node g.outputPorts.no-flip text.middle, g.node g.exitPorts.no-flip text.middle, g.node g.inputPorts.flipped text.middle { - text-anchor: middle; -} - -g.node g.outputLocalPorts.no-flip text, g.node g.exitLocalPorts.no-flip text, g.node g.inputLocalPorts.flipped text { - text-anchor: end; - font-family: sans-serif; - pointer-events: none; - fill: grey; - user-select: none; -} - -g.node g.inputPorts text.event, g.node g.outputPorts text.event, g.node g.exitPorts text.event { - fill: mediumblue; -} - -g.node g.inputLocalPorts text.event, g.node g.outputLocalPorts text.event, g.node g.exitLocalPorts text.event { - fill: cornflowerblue; -} - -line.link, line.draggingLink, line.autoCompleteLink, path.link, path.draggingLink, path.autoCompleteLink { - stroke-width: 2; - pointer-events: none; -} - -line.linkExtra { - stroke-width: 8; - stroke-opacity: 0; -} diff --git a/templates/base.html b/templates/base.html index f78bd0fe7..1a6d3d649 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,6 +11,7 @@ + @@ -24,7 +25,6 @@ require(["showdown"]); require(["bindingHandlers/readonly"]); require(["bindingHandlers/disabled"]); - require(["bindingHandlers/graphRenderer"]); require(["bindingHandlers/eagleTooltip"]); require(["bindingHandlers/eagleRightClick"]); require(["components"]); @@ -75,11 +75,65 @@
    -
    -
    +
    +
    + + + {% include 'node.html' %} + +
    + +
    + +
    + + alarm_on + +
    +
    +
    +
    + + +
    + + alarm_on + +
    +
    +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    - +
    +
    +
    +
    +
    + +
    + +
    +
    +
    diff --git a/templates/branch_node.html b/templates/branch_node.html new file mode 100644 index 000000000..0d85ad049 --- /dev/null +++ b/templates/branch_node.html @@ -0,0 +1,14 @@ +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    diff --git a/templates/comment_node.html b/templates/comment_node.html new file mode 100644 index 000000000..5e4792297 --- /dev/null +++ b/templates/comment_node.html @@ -0,0 +1,12 @@ +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    diff --git a/templates/construct_node.html b/templates/construct_node.html new file mode 100644 index 000000000..ba3d41b0c --- /dev/null +++ b/templates/construct_node.html @@ -0,0 +1,22 @@ +
    +
    +
    +
    +
    + +
    + +
    +
    + + +
    + {% include 'basic_node.html' %} +
    + + +
    + {% include 'basic_node.html' %} +
    + +
    \ No newline at end of file diff --git a/templates/edge_inspector.html b/templates/edge_inspector.html index 67614cf95..418c6b38e 100644 --- a/templates/edge_inspector.html +++ b/templates/edge_inspector.html @@ -87,20 +87,25 @@
    - -
    - + + +
    +
    - - + + + + - - + + + + - +
    diff --git a/templates/modals.html b/templates/modals.html index 18a0c380f..00b832e67 100644 --- a/templates/modals.html +++ b/templates/modals.html @@ -298,7 +298,8 @@
    Editor for the Advanced Graph Language Environment
    -{% include 'modals/errors.html' %} +{% include 'modals/action_list.html' %} +{% include 'modals/check_graph.html' %} {% include 'modals/settings.html' %} {% include 'modals/shortcuts.html' %} diff --git a/templates/modals/action_list.html b/templates/modals/action_list.html new file mode 100644 index 000000000..6d6a1212e --- /dev/null +++ b/templates/modals/action_list.html @@ -0,0 +1,103 @@ + + \ No newline at end of file diff --git a/templates/modals/check_graph.html b/templates/modals/check_graph.html new file mode 100644 index 000000000..9e0d41e75 --- /dev/null +++ b/templates/modals/check_graph.html @@ -0,0 +1,76 @@ + + \ No newline at end of file diff --git a/templates/modals/errors.html b/templates/modals/errors.html deleted file mode 100644 index 93d7ad153..000000000 --- a/templates/modals/errors.html +++ /dev/null @@ -1,72 +0,0 @@ - - \ No newline at end of file diff --git a/templates/modals/settings.html b/templates/modals/settings.html index 1a8a78679..1b6130cf2 100644 --- a/templates/modals/settings.html +++ b/templates/modals/settings.html @@ -101,31 +101,31 @@
    - Translator url + Translator url
    - GitHub Access Token + GitHub Access Token
    - GitLab Access Token + GitLab Access Token
    - Docker Hub Username + Docker Hub Username
    - Explore Palettes Repository + Explore Palettes Repository
    @@ -134,7 +134,7 @@
    diff --git a/templates/navbar.html b/templates/navbar.html index b0288f8dd..12390bab7 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -5,6 +5,7 @@
    EAGLE + π {{version}}
    @@ -36,31 +37,39 @@ + View + +
    diff --git a/templates/node.html b/templates/node.html new file mode 100644 index 000000000..9082603d3 --- /dev/null +++ b/templates/node.html @@ -0,0 +1,14 @@ +
    + + {% include 'basic_node.html' %} + + + {% include 'branch_node.html' %} + + + {% include 'construct_node.html' %} + + + {% include 'comment_node.html' %} + +
    diff --git a/templates/node_inspector.html b/templates/node_inspector.html index 2cfc9ab32..ff97578b5 100644 --- a/templates/node_inspector.html +++ b/templates/node_inspector.html @@ -161,14 +161,16 @@
    - - - + + + - - + + + + diff --git a/templates/palettes.html b/templates/palettes.html index dd84a20b6..a20dc1b38 100644 --- a/templates/palettes.html +++ b/templates/palettes.html @@ -71,7 +71,7 @@
    -
    +
     
    diff --git a/tests/add-node-parameter.js b/tests/add-node-parameter.js index f0d5a1349..498a4aa3f 100644 --- a/tests/add-node-parameter.js +++ b/tests/add-node-parameter.js @@ -36,6 +36,9 @@ test('Add node parameter with usage InputPort', async t =>{ .wait(1000) + //confirm creating new graph + .click('#inputModal .affermativeBtn') + const numInputPortsBefore = await getNumInputPorts(); diff --git a/tests/component-update.js b/tests/component-update.js index d96a88107..7a2018c78 100644 --- a/tests/component-update.js +++ b/tests/component-update.js @@ -48,7 +48,9 @@ test('Update components', async t =>{ // !!!!!!!!!!!!! Click the "component update" button await t - .click(Selector('#checkForComponentUpdates')); + .click(Selector('#checkForComponentUpdates')) + .click(Selector('#actionListModalPerformAll')) + .click(Selector('#actionListModalOK')); await t.wait(3000); // !!!!!!!!!!!!! EXPORT JSON diff --git a/tests/edit-edge.js b/tests/edit-edge.js index 00f1804dc..fc7f7304f 100644 --- a/tests/edit-edge.js +++ b/tests/edit-edge.js @@ -35,6 +35,11 @@ test('Change destination port used by edge', async t =>{ // add File component to graph .click('#addPaletteNodeFile') + .wait(1000) + + //confirm creating new graph + .click('#inputModal .affermativeBtn') + // add CopyApp component to graph .click('#addPaletteNodeCopyApp')