├── docs ├── res │ ├── example1.png │ └── gerbs │ │ ├── RU-AD.png │ │ ├── RU-AL.png │ │ ├── RU-BA.png │ │ ├── RU-BU.png │ │ ├── RU-CE.png │ │ ├── RU-CU.png │ │ ├── RU-DA.png │ │ ├── RU-IN.png │ │ ├── RU-KB.png │ │ ├── RU-KC.png │ │ ├── RU-KK.png │ │ ├── RU-KL.png │ │ ├── RU-KO.png │ │ ├── RU-KR.png │ │ ├── RU-ME.png │ │ ├── RU-MO.png │ │ ├── RU-SA.png │ │ ├── RU-SE.png │ │ ├── RU-TA.png │ │ ├── RU-TY.png │ │ ├── RU-UD.png │ │ ├── UA-40.png │ │ ├── UA-43.png │ │ ├── RU-ALT.png │ │ ├── RU-AMU.png │ │ ├── RU-ARK.png │ │ ├── RU-AST.png │ │ ├── RU-BEL.png │ │ ├── RU-BRY.png │ │ ├── RU-CHE.png │ │ ├── RU-CHU.png │ │ ├── RU-IRK.png │ │ ├── RU-IVA.png │ │ ├── RU-KAM.png │ │ ├── RU-KDA.png │ │ ├── RU-KEM.png │ │ ├── RU-KGD.png │ │ ├── RU-KGN.png │ │ ├── RU-KHA.png │ │ ├── RU-KHM.png │ │ ├── RU-KIR.png │ │ ├── RU-KLU.png │ │ ├── RU-KOS.png │ │ ├── RU-KRS.png │ │ ├── RU-KRY.png │ │ ├── RU-KYA.png │ │ ├── RU-LEN.png │ │ ├── RU-LIP.png │ │ ├── RU-MAG.png │ │ ├── RU-MOS.png │ │ ├── RU-MOW.png │ │ ├── RU-MUR.png │ │ ├── RU-NEN.png │ │ ├── RU-NGR.png │ │ ├── RU-NIZ.png │ │ ├── RU-NVS.png │ │ ├── RU-OMS.png │ │ ├── RU-ORE.png │ │ ├── RU-ORL.png │ │ ├── RU-PER.png │ │ ├── RU-PNZ.png │ │ ├── RU-PRI.png │ │ ├── RU-PSK.png │ │ ├── RU-ROS.png │ │ ├── RU-RYA.png │ │ ├── RU-SAK.png │ │ ├── RU-SAM.png │ │ ├── RU-SAR.png │ │ ├── RU-SEV.png │ │ ├── RU-SMO.png │ │ ├── RU-SPE.png │ │ ├── RU-STA.png │ │ ├── RU-SVE.png │ │ ├── RU-TAM.png │ │ ├── RU-TOM.png │ │ ├── RU-TUL.png │ │ ├── RU-TVE.png │ │ ├── RU-TYU.png │ │ ├── RU-ULY.png │ │ ├── RU-VGG.png │ │ ├── RU-VLA.png │ │ ├── RU-VLG.png │ │ ├── RU-VOR.png │ │ ├── RU-YAN.png │ │ ├── RU-YAR.png │ │ ├── RU-YEV.png │ │ └── RU-ZAB.png ├── example-text │ └── index.html └── example-img │ └── index.html ├── .eslintignore ├── .gitignore ├── src ├── util │ ├── templateFilterStorage │ │ └── dotColor.js │ ├── transformHexToRGB.js │ ├── getPolygonWithMaxArea.js │ ├── State.js │ ├── zoom │ │ ├── setZoomVisibility.js │ │ ├── parseZoomData.js │ │ └── getFirstZoomInside.js │ ├── transformPolygonCoords.js │ ├── checkPointPosition.js │ └── getPolesOfInaccessibility.js ├── label │ ├── util │ │ ├── getLayoutSize.js │ │ ├── layoutTemplates │ │ │ ├── getBaseLayoutTemplates.js │ │ │ ├── dotLayoutTemplate.js │ │ │ ├── labelLayoutTemplate.js │ │ │ └── createLayoutTemplates.js │ │ ├── classHelper.js │ │ ├── LabelPlacemarkOverlay.js │ │ └── LabelData.js │ ├── LabelBase.js │ ├── GeoObjectCollection │ │ └── Label.js │ └── ObjectManager │ │ └── Label.js ├── config.js ├── create.js └── polylabel │ ├── PolylabelBase.js │ ├── PolylabelCollection.js │ └── PolylabelObjectManager.js ├── package.json ├── .eslintrc.js ├── LICENSE.md ├── Gruntfile.js ├── .babelrc └── README.md /docs/res/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/example1.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-AD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-AD.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-AL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-AL.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-BA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-BA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-BU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-BU.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-CE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-CE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-CU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-CU.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-DA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-DA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-IN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-IN.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KB.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KC.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KK.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KL.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KO.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ME.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-MO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-MO.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TY.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-UD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-UD.png -------------------------------------------------------------------------------- /docs/res/gerbs/UA-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/UA-40.png -------------------------------------------------------------------------------- /docs/res/gerbs/UA-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/UA-43.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ALT.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-AMU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-AMU.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ARK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ARK.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-AST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-AST.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-BEL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-BEL.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-BRY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-BRY.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-CHE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-CHE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-CHU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-CHU.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-IRK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-IRK.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-IVA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-IVA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KAM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KAM.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KDA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KDA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KEM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KEM.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KGD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KGD.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KGN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KGN.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KHA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KHA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KHM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KHM.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KIR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KIR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KLU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KLU.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KOS.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KRS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KRS.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KRY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KRY.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-KYA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-KYA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-LEN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-LEN.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-LIP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-LIP.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-MAG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-MAG.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-MOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-MOS.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-MOW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-MOW.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-MUR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-MUR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-NEN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-NEN.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-NGR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-NGR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-NIZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-NIZ.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-NVS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-NVS.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-OMS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-OMS.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ORE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ORE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ORL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ORL.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-PER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-PER.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-PNZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-PNZ.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-PRI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-PRI.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-PSK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-PSK.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ROS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ROS.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-RYA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-RYA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SAK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SAK.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SAM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SAM.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SAR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SEV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SEV.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SMO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SMO.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SPE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SPE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-STA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-STA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-SVE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-SVE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TAM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TAM.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TOM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TOM.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TUL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TUL.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TVE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TVE.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-TYU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-TYU.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ULY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ULY.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-VGG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-VGG.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-VLA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-VLA.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-VLG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-VLG.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-VOR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-VOR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-YAN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-YAN.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-YAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-YAR.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-YEV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-YEV.png -------------------------------------------------------------------------------- /docs/res/gerbs/RU-ZAB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandex/mapsapi-polylabeler/master/docs/res/gerbs/RU-ZAB.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cases/ 3 | node_modules/ 4 | Gruntfile.js 5 | src/calculateArea.min.js 6 | karma.conf.js 7 | builder/ 8 | test/ 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build 3 | cases/ 4 | .idea 5 | polylabel/ 6 | mapsapi-polylabeler/ 7 | npm-debug.log 8 | ._.DS_Store 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /src/util/templateFilterStorage/dotColor.js: -------------------------------------------------------------------------------- 1 | import transformHexToRGB from '../transformHexToRGB'; 2 | import CONFIG from '../../config'; 3 | 4 | export default function (data, dateString) { 5 | return transformHexToRGB(dateString || CONFIG.DEFAULT_POLYGON_FILL_COLOR, 0.9); 6 | } 7 | -------------------------------------------------------------------------------- /src/label/util/getLayoutSize.js: -------------------------------------------------------------------------------- 1 | export default function (layout) { 2 | let el = layout && layout.getElement(); 3 | if (!el) return; 4 | 5 | el = el.getElementsByClassName('ymaps-polylabel-view')[0]; 6 | if (!el) return; 7 | 8 | const {width, height} = el.getBoundingClientRect(); 9 | return {width, height}; 10 | } 11 | -------------------------------------------------------------------------------- /src/label/util/layoutTemplates/getBaseLayoutTemplates.js: -------------------------------------------------------------------------------- 1 | import labelLayoutTemplate from './labelLayoutTemplate'; 2 | import dotLayoutTemplate from './dotLayoutTemplate'; 3 | 4 | /** 5 | * Returns basic wrappers over templates 6 | */ 7 | export default function () { 8 | return { 9 | label: labelLayoutTemplate, 10 | dot: dotLayoutTemplate 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/label/util/layoutTemplates/dotLayoutTemplate.js: -------------------------------------------------------------------------------- 1 | import templateLayoutFactory from 'api/templateLayoutFactory'; 2 | 3 | const template = templateLayoutFactory.createClass( 4 | `
6 | {% include options.labelDotTemplateLayout %} 7 |
` 8 | ); 9 | 10 | export default template; 11 | -------------------------------------------------------------------------------- /src/util/transformHexToRGB.js: -------------------------------------------------------------------------------- 1 | export default function (hex, opacity) { 2 | if (!hex) return; 3 | if (hex.indexOf('rgb') !== -1) return hex; 4 | hex = hex[0] !== '#' ? `#${hex}` : hex; 5 | hex = hex.slice(0, 7); 6 | let c; 7 | if (!/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) return; 8 | 9 | c = hex.substring(1).split(''); 10 | if (c.length === 3) { 11 | c = [c[0], c[0], c[1], c[1], c[2], c[2]]; 12 | } 13 | c = '0x' + c.join(''); 14 | return `rgba(${[(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',')}, ${opacity || 1})`; 15 | } 16 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | MIN_ZOOM: 0, 3 | MAX_ZOOM: 23, 4 | DEFAULT_POLYGON_FILL_COLOR: '0066ff99', 5 | options: [ 6 | 'labelLayout', 7 | 'labelDotLayout', 8 | 'labelDotVisible', 9 | 'labelDefaults', 10 | 'labelCursor', 11 | 'labelDotCursor' 12 | ], 13 | zoomRangeOptions: [ 14 | 'labelClassName', 15 | 'labelForceVisible', 16 | 'labelTextColor', 17 | 'labelTextSize', 18 | 'labelCenterCoords', 19 | 'labelOffset', 20 | 'labelPermissibleInaccuracyOfVisibility' 21 | ], 22 | properties: [] 23 | }; 24 | -------------------------------------------------------------------------------- /src/label/util/layoutTemplates/labelLayoutTemplate.js: -------------------------------------------------------------------------------- 1 | import templateLayoutFactory from 'api/templateLayoutFactory'; 2 | 3 | const template = templateLayoutFactory.createClass(` 4 |
6 |
9 | {% include options.labelTemplateLayout %} 10 |
11 |
` 12 | ); 13 | 14 | export default template; 15 | -------------------------------------------------------------------------------- /src/label/util/layoutTemplates/createLayoutTemplates.js: -------------------------------------------------------------------------------- 1 | import templateLayoutFactory from 'api/templateLayoutFactory'; 2 | 3 | /** 4 | * Creates custom templates 5 | */ 6 | export default function (labelLayout, labelDotLayout) { 7 | const dotDefault = `
`; 10 | return { 11 | label: { 12 | iconLabelTemplateLayout: templateLayoutFactory.createClass(labelLayout) 13 | }, 14 | dot: { 15 | iconLabelDotTemplateLayout: templateLayoutFactory.createClass(labelDotLayout || dotDefault) 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapsapi-polylabeler", 3 | "version": "1.0.2", 4 | "description": "", 5 | "scripts": { 6 | "build": "grunt build", 7 | "lint": "eslint ." 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "babel-core": "^6.26.0", 14 | "babel-plugin-transform-class-properties": "^6.24.1", 15 | "babel-plugin-transform-es2015-modules-ym": "^0.3.0", 16 | "babel-preset-env": "^1.6.0", 17 | "eslint": "^4.5.0", 18 | "glob": "^7.1.2", 19 | "grunt": "1.0.1", 20 | "grunt-babel": "^7.0.0", 21 | "grunt-contrib-concat": "^1.0.1", 22 | "grunt-contrib-uglify": "3.0.1", 23 | "grunt-contrib-watch": "1.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "loris/es5", 3 | "root": true, 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "rules": { 10 | "strict": ["error", "never"], 11 | "curly": [0, "multi"], 12 | "no-console": ["error", { allow: ["error"] }], 13 | "no-implicit-globals": 0, 14 | "no-cond-assign": 0 15 | }, 16 | "globals": { 17 | "modules": true, 18 | "module": true, 19 | "ymaps": true, 20 | "chai": true, 21 | "describe": true, 22 | "modules": true, 23 | "expect": true, 24 | "it": true, 25 | "before": true, 26 | "after": true 27 | }, 28 | "parserOptions": { 29 | "sourceType": "module" 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/util/getPolygonWithMaxArea.js: -------------------------------------------------------------------------------- 1 | import calculateArea from 'api/util.calculateArea'; 2 | import GeoObject from 'api/GeoObject'; 3 | 4 | export default function (polygonCoords) { 5 | if (typeof calculateArea === 'undefined') { 6 | throw new Error('Didn\'t find calculateArea module'); 7 | } 8 | let maxArea = Number.MIN_VALUE; 9 | let indexOfMaxArea = 0; 10 | for (let i = 0; i < polygonCoords.length; i++) { 11 | let polygon = new GeoObject({ 12 | geometry: { 13 | type: 'Polygon', coordinates: [polygonCoords[i]] 14 | } 15 | }); 16 | let area = Math.round(calculateArea(polygon)); 17 | if (maxArea < area) { 18 | maxArea = area; 19 | indexOfMaxArea = i; 20 | } 21 | } 22 | return polygonCoords[indexOfMaxArea]; 23 | } 24 | -------------------------------------------------------------------------------- /src/util/State.js: -------------------------------------------------------------------------------- 1 | import DataManager from 'api/data.Manager'; 2 | 3 | export default class State { 4 | constructor() { 5 | this._state = new WeakMap(); 6 | } 7 | 8 | _getDataManager(polygon) { 9 | let dataManager = this._state.get(polygon); 10 | if (!dataManager) { 11 | dataManager = new DataManager(); 12 | this._state.set(polygon, dataManager); 13 | } 14 | return dataManager; 15 | } 16 | 17 | set(polygon, key, value) { 18 | const dataManager = this._getDataManager(polygon); 19 | dataManager.set(key, value); 20 | } 21 | 22 | get(polygon, key) { 23 | const dataManager = this._getDataManager(polygon); 24 | if (dataManager) { 25 | return dataManager.get(key); 26 | } 27 | } 28 | 29 | getState(polygon) { 30 | return this._getDataManager(polygon); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/label/util/classHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} layout 3 | * @param {*} className - class by which we find the element 4 | * @param {*} newClassName - new class 5 | */ 6 | function add(layout, className, newClassName) { 7 | const el = getElemByClass(layout, className); 8 | if (!el) return; 9 | 10 | el.classList.add(newClassName); 11 | } 12 | 13 | /** 14 | * @param {*} layout 15 | * @param {*} className - class by which we find the element 16 | * @param {*} removeClassName - class to remove 17 | */ 18 | function remove(layout, className, removeClassName) { 19 | const el = getElemByClass(layout, className); 20 | if (!el) return; 21 | 22 | el.classList.remove(removeClassName); 23 | } 24 | 25 | function getElemByClass(layout, className) { 26 | if (!layout) return; 27 | let el = layout.getElement(); 28 | if (!el) return; 29 | 30 | return el.getElementsByClassName(className)[0]; 31 | } 32 | 33 | export default { 34 | add, 35 | remove 36 | }; 37 | -------------------------------------------------------------------------------- /src/label/util/LabelPlacemarkOverlay.js: -------------------------------------------------------------------------------- 1 | import overlayPlacemark from 'api/overlay.Placemark'; 2 | import GeoObject from 'api/GeoObject'; 3 | 4 | /** 5 | * Puts the data into a label from the polygon 6 | */ 7 | export default class LabelPlacemarkOverlay extends overlayPlacemark { 8 | constructor(geometry, properties, options) { 9 | super(geometry, properties, options); 10 | } 11 | 12 | getData() { 13 | const polygon = this._data.geoObject instanceof GeoObject ? 14 | this._data.geoObject.properties.get('polygon') : 15 | this._data.properties.polygon; 16 | 17 | return { 18 | geoObject: polygon, 19 | properties: polygon.properties, 20 | //options: polygon.options, TODO невозможно переопределить опции, потому что https://github.yandex-team.ru/mapsapi/jsapi-v2/blob/master/src/overlay/view/abstract/baseWithLayout/overlay.view.BaseWithLayout.js#L99 21 | state: polygon.state 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 YANDEX LLC 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/util/zoom/setZoomVisibility.js: -------------------------------------------------------------------------------- 1 | import getFirstZoomInside from './getFirstZoomInside'; 2 | import getLayoutSize from '../../label/util/getLayoutSize'; 3 | 4 | export default function ( 5 | labelType, map, zoom, currVisible, layout, center, 6 | polygonCoordinates, labelOffset, permissibleInaccuracyOfVisibility 7 | ) { 8 | return analyze( 9 | labelType, 10 | map, 11 | zoom, 12 | currVisible, 13 | layout, 14 | center, 15 | polygonCoordinates, 16 | labelOffset, 17 | permissibleInaccuracyOfVisibility 18 | ); 19 | } 20 | 21 | function getVisible(currentType, newType, newIsVisible) { 22 | let types = ['none', 'dot', 'label']; 23 | let result = currentType; 24 | if (newIsVisible) { 25 | result = types.indexOf(newType) > types.indexOf(currentType) ? newType : currentType; 26 | } 27 | return result; 28 | } 29 | 30 | function analyze(type, map, zoom, currVisible, layout, center, 31 | polygonCoordinates, labelOffset, permissibleInaccuracyOfVisibility) { 32 | const size = getLayoutSize(layout); 33 | if (!size || size.width === 0 || size.height === 0) return; 34 | 35 | const firstZoomInside = getFirstZoomInside( 36 | map, 37 | center, 38 | polygonCoordinates, 39 | size, 40 | labelOffset, 41 | type === 'dot' ? 0 : permissibleInaccuracyOfVisibility 42 | ); 43 | 44 | return { 45 | visible: getVisible(currVisible, type, zoom >= firstZoomInside), 46 | size 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | const glob = require("glob"); 2 | module.exports = function (grunt) { 3 | const fileNames = glob.sync("src/**/*.js"); 4 | const babelFiles = fillFiles(fileNames); 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | babel: { 8 | options: { 9 | sourceMap: true 10 | }, 11 | dist: { 12 | files: babelFiles 13 | } 14 | }, 15 | concat: { 16 | build: { 17 | src: [ 18 | 'build/pre/**/*.js' 19 | ], 20 | dest: 'build/polylabel.js' 21 | } 22 | }, 23 | uglify: { 24 | build: { 25 | src: 'build/polylabel.js', 26 | dest: 'build/polylabel.min.js' 27 | } 28 | }, 29 | watch: { 30 | files: ['src/**/*.js'], 31 | tasks: 'default' 32 | } 33 | }); 34 | grunt.loadNpmTasks('grunt-babel'); 35 | grunt.loadNpmTasks('grunt-contrib-concat'); 36 | grunt.loadNpmTasks('grunt-contrib-uglify'); 37 | grunt.loadNpmTasks('grunt-contrib-watch'); 38 | 39 | grunt.registerTask('build', ['babel', 'concat', 'uglify']); 40 | grunt.registerTask('default', ['build', 'watch']); 41 | }; 42 | 43 | function fillFiles(names) { 44 | names = names.map(name => { 45 | return name.split('/').slice(1).join('/'); 46 | }); 47 | let result = {}; 48 | names.forEach((fileName) => { 49 | result[`build/pre/${fileName}`] = `src/${fileName}`; 50 | }); 51 | return result; 52 | } 53 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "sourceRoot": "src/", 4 | "presets": [ 5 | [ 6 | "env", 7 | { 8 | "targets": [ 9 | "last 5 version", 10 | "ie >= 8", 11 | "safari >= 7" 12 | ], 13 | "loose": true, 14 | "spec": false, 15 | "strict": false, 16 | "noInterop": true, 17 | "modules": false 18 | } 19 | ] 20 | ], 21 | "plugins": [ 22 | [ 23 | "transform-class-properties", 24 | { 25 | "spec": false 26 | } 27 | ], 28 | [ 29 | "./builder/plugin-transform-ym-helpers", 30 | { 31 | "overrideHelpers": { 32 | "inheritsLoose": "util.defineClass", 33 | "inherits": "util.defineClass", 34 | "extends": "util.extend" 35 | }, 36 | "replaceGlobals": { 37 | "Object.assign": "util.extend", 38 | "Array.isArray": [ 39 | "util.array", 40 | "isArray" 41 | ], 42 | "WeakMap": "util.WeakMap", 43 | "Promise": [ 44 | "vow", 45 | "Promise" 46 | ] 47 | } 48 | } 49 | ], 50 | [ 51 | "babel-plugin-transform-es2015-modules-ym", 52 | { 53 | "moduleBase": "polylabel", 54 | "sourceDir": "src", 55 | "ymGlobal": "ymaps", 56 | "sourceMappings": { 57 | "api": "" 58 | } 59 | } 60 | ] 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/util/transformPolygonCoords.js: -------------------------------------------------------------------------------- 1 | // these functions are needed to transform the coordinates of the polygon, 2 | // because there are transitions from 180 to -180, 3 | // that violates the function of calculating the center 4 | const methods = { 5 | polygon: polygon, 6 | point: point 7 | }; 8 | 9 | function point(coords, isPositivePart) { 10 | const key = getKey(coords); 11 | return transformPoint(isPositivePart, key, coords); 12 | } 13 | 14 | function polygon(coords) { 15 | let result = []; 16 | let isPositivePart = true; // true = positive, false = negative 17 | for (let i = 0; i < coords.length; i++) { 18 | const pointCoords = coords[i]; 19 | if (i === 0) { 20 | isPositivePart = pointCoords[1] >= 0 ? true : false; 21 | } 22 | const key = getKey(pointCoords); 23 | result.push(transformPoint(isPositivePart, key, pointCoords)); 24 | } 25 | return { 26 | coords: result, 27 | isPositivePart 28 | }; 29 | } 30 | 31 | function transformPoint(isPositivePart, key, pointCoords) { 32 | return isPositivePart ? transformPositive(key, pointCoords) : transformNegative(key, pointCoords); 33 | } 34 | 35 | /** 36 | * Gets the key, which says which of the seams is closer 37 | */ 38 | function getKey(p) { 39 | const arr = [ 40 | { 41 | key: '180', 42 | distance: 180 - p[1] 43 | }, 44 | { 45 | key: '-180', 46 | distance: Math.abs(-180 - p[1]) 47 | }, 48 | { 49 | key: '0', 50 | distance: Math.abs(p[1]) 51 | } 52 | ]; 53 | return arr.sort(comparator)[0].key; 54 | } 55 | 56 | function transformPositive(key, point) { 57 | return (key === '-180') ? [point[0], 360 + point[1]] : point; 58 | } 59 | 60 | function transformNegative(key, point) { 61 | return (key === '180') ? [point[0], -360 + point[1]] : point; 62 | } 63 | 64 | function comparator(a, b) { 65 | return a.distance - b.distance; 66 | } 67 | 68 | export default methods; 69 | -------------------------------------------------------------------------------- /src/util/zoom/parseZoomData.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config'; 2 | const {MIN_ZOOM, MAX_ZOOM} = CONFIG; 3 | 4 | /** 5 | * Parse data about zoom. 6 | * @param {Object|primitive} zoomData 7 | * Supported object properties view: number, string 8 | * @return {Object} - Returned object with zoom, where the parsed values. 9 | * @example 10 | * zoomData = {1: 'value1', '3_5': 'value2'} 11 | * return {1: 'value1', 2: undefined ... 3: 'value2', 4: 'value2', 5: 'value2', 6: undefined ...} 12 | * zoomData = 'value123' 13 | * return {1: 'value123' ... 19: 'value123'} 14 | */ 15 | export default function parseZoomData(zoomData) { 16 | const valid = ['number', 'string', 'boolean', 'object']; 17 | if (zoomData && !Array.isArray(zoomData) && typeof zoomData === 'object') { 18 | return Object.keys(zoomData).reduce((result, key) => { 19 | if (typeof key === 'string') { 20 | parseString(result, key, zoomData[key]); 21 | } else if (typeof key === 'number') { 22 | parseNumber(result, key, zoomData[key]); 23 | } 24 | return result; 25 | }, createDefZoomObj()); 26 | } else if (valid.indexOf(typeof zoomData) !== -1) { 27 | return createDefZoomObj(zoomData); 28 | } 29 | } 30 | 31 | function parseNumber(target, zoom, value) { 32 | target[zoom] = value; 33 | } 34 | 35 | function parseString(target, zoom, value) { 36 | if (!isNaN(Number(zoom))) { 37 | target[Number(zoom)] = value; 38 | return; 39 | } 40 | const zoomRange = zoom.split('_').map(Number); 41 | if (isNaN(zoomRange[0]) || isNaN(zoomRange[1])) { 42 | return; 43 | } 44 | let bottom = zoomRange[0] < MIN_ZOOM ? MIN_ZOOM : zoomRange[0]; 45 | const top = zoomRange[1] > MAX_ZOOM ? MAX_ZOOM : zoomRange[1]; 46 | while (bottom <= top) { 47 | target[bottom] = value; 48 | bottom++; 49 | } 50 | } 51 | 52 | function createDefZoomObj(val) { 53 | let result = {}; 54 | for (let i = MIN_ZOOM; i <= MAX_ZOOM; i++) { 55 | result[i] = val; 56 | } 57 | return result; 58 | } 59 | -------------------------------------------------------------------------------- /src/create.js: -------------------------------------------------------------------------------- 1 | import PCollection from './polylabel/PolylabelCollection'; 2 | import PObjectManager from './polylabel/PolylabelObjectManager'; 3 | import ObjectManager from 'api/ObjectManager'; 4 | 5 | export default function (map, data) { 6 | initStyles(); 7 | return data instanceof ObjectManager ? 8 | new PObjectManager(map, data) : 9 | new PCollection(map, data); 10 | } 11 | 12 | function initStyles() { 13 | const style = document.createElement('style'); 14 | style.innerText = ` 15 | .ymaps-polylabel-dot-default { 16 | border: 1px solid rgba(255, 255, 255, 0.8); 17 | height: 4px; 18 | width: 4px; 19 | border-radius: 3px; 20 | } 21 | .ymaps-polylabel-dot-default_hover { 22 | border: 1px solid rgba(255, 255, 255, 1); 23 | } 24 | .ymaps-polylabel-label-default { 25 | font-family: Arial, Helvetica, sans-serif; 26 | text-align: center; 27 | } 28 | .ymaps-polylabel-dark-label { 29 | color: #06264f; 30 | -webkit-font-smoothing: subpixel-antialiased; 31 | text-shadow: 32 | 0 -1px 0 rgba( 255, 255, 255, 0.4 ), 33 | 0 -1px 0 rgba( 255, 255, 255, 0.4 ), 34 | 0 1px 0 rgba( 255, 255, 255, 0.4 ), 35 | 0 1px 0 rgba( 255, 255, 255, 0.4 ), 36 | -1px 0 0 rgba( 255, 255, 255, 0.4 ), 37 | 1px 0 0 rgba( 255, 255, 255, 0.4 ), 38 | -1px 0 0 rgba( 255, 255, 255, 0.4 ), 39 | 1px 0 0 rgba( 255, 255, 255, 0.4 ), 40 | -1px -1px 0 rgba( 255, 255, 255, 0.4 ), 41 | 1px -1px 0 rgba( 255, 255, 255, 0.4 ), 42 | -1px 1px 0 rgba( 255, 255, 255, 0.4 ), 43 | 1px 1px 0 rgba( 255, 255, 255, 0.4 ), 44 | -1px -1px 0 rgba( 255, 255, 255, 0.4 ), 45 | 1px -1px 0 rgba( 255, 255, 255, 0.4 ), 46 | -1px 1px 0 rgba( 255, 255, 255, 0.4 ), 47 | 1px 1px 0 rgba( 255, 255, 255, 0.4 ); 48 | opacity: 0.9; 49 | } 50 | .ymaps-polylabel-light-label { 51 | color: rgba(255, 255, 255, 1); 52 | text-shadow: 0 0 2px rgba(0, 0, 0, 1), 0 0 2px rgba(0, 0, 0, 1); 53 | } 54 | `.replace(/\n+\s+/gi, ''); 55 | document.head.appendChild(style); 56 | } 57 | -------------------------------------------------------------------------------- /src/util/checkPointPosition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if the point is inside the polygon. 3 | * @param {Array[2]} point 4 | * @param {Array} coords - Polygon coords. 5 | */ 6 | export default function isInside(point, coords) { 7 | let parity = 0; 8 | for (let i = 0; i < coords.length - 1; i++) { 9 | let e = [coords[i], coords[i + 1]]; 10 | switch (edgeType(point, e)) { 11 | case 'TOUCHING': 12 | return 'BOUNDARY'; 13 | case 'CROSSING': 14 | parity = 1 - parity; 15 | } 16 | } 17 | return (parity ? 'INSIDE' : 'OUTSIDE'); 18 | } 19 | 20 | /** 21 | * Determines the position of the point relative to the edge. 22 | * @param {Array[2]} p - The investigated point. 23 | * @param {Array[2]} p0 - The first point of the edge. 24 | * @param {Array[2]} p1 - The second point of the edge. 25 | */ 26 | function pointClassify(p, p0, p1) { 27 | const a = pointMinus(p1, p0); 28 | const b = pointMinus(p, p0); 29 | const sa = a[0] * b[1] - b[0] * a[1]; 30 | if (sa > 0) { 31 | return 'LEFT'; 32 | } 33 | if (sa < 0) { 34 | return 'RIGHT'; 35 | } 36 | if ((a[0] * b[0] < 0) || (a[1] * b[1] < 0)) { 37 | return 'BEHIND'; 38 | } 39 | if (pointLength(a) < pointLength(b)) { 40 | return 'BEYOND'; 41 | } 42 | if (pointEquals(p0, p)) { 43 | return 'ORIGIN'; 44 | } 45 | if (pointEquals(p1, p)) { 46 | return 'DESTINATION'; 47 | } 48 | return 'BETWEEN'; 49 | } 50 | 51 | function pointMinus(p1, p2) { 52 | return [p1[0] - p2[0], p1[1] - p2[1]]; 53 | } 54 | 55 | function pointLength(p) { 56 | return Math.sqrt(Math.pow(p[0], 2) + Math.pow(p[1], 2)); 57 | } 58 | 59 | function pointEquals(p1, p2) { 60 | return p1[0] === p2[0] && p1[1] === p2[1]; 61 | } 62 | 63 | /** 64 | * Determines the positions of the ray released from the point relative to the edge (Crosses, Affects, Neutral). 65 | * @param {Arrya[2]} point - The investigated point. 66 | * @param {Array} edge - Edge. 67 | */ 68 | function edgeType(point, edge) { 69 | const v = edge[0]; 70 | const w = edge[1]; 71 | switch (pointClassify(point, v, w)) { 72 | case 'LEFT': { 73 | return ((v[1] < point[1]) && (point[1] <= w[1])) ? 'CROSSING' : 'INESSENTIAL'; 74 | } 75 | case 'RIGHT': { 76 | return ((w[1] < point[1]) && (point[1] <= v[1])) ? 'CROSSING' : 'INESSENTIAL'; 77 | } 78 | case 'BETWEEN': 79 | case 'ORIGIN': 80 | case 'DESTINATION': 81 | return 'TOUCHING'; 82 | default: { 83 | return 'INESSENTIAL'; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/util/zoom/getFirstZoomInside.js: -------------------------------------------------------------------------------- 1 | import isInside from '../checkPointPosition'; 2 | import CONFIG from '../../config'; 3 | 4 | export default function (map, center, coords, size, offset, resolvedInaccuracy) { 5 | let {MIN_ZOOM: i, MAX_ZOOM: j} = CONFIG; 6 | let zoom; 7 | while (i < j) { 8 | zoom = Math.floor((i + j) / 2); 9 | const ri = isNaN(Number(resolvedInaccuracy)) ? 0 : Number(resolvedInaccuracy); 10 | const elemPoints = getElemPoints(map, center, zoom, size, offset || [0, 0], ri); 11 | if (checkIsInside(map, coords, elemPoints.normal, zoom) || 12 | (ri !== 0 && checkIsInside(map, coords, elemPoints.withInaccuracy, zoom))) { 13 | j = zoom; 14 | } else { 15 | i = zoom + 1; 16 | } 17 | } 18 | return i; 19 | } 20 | 21 | function getElemPoints(map, center, zoom, size, offset, ri) { 22 | const centerProj = map.options.get('projection').toGlobalPixels(center, zoom); 23 | let {width: w, height: h} = size; 24 | 25 | centerProj[0] += offset[0]; 26 | centerProj[1] += offset[1]; 27 | 28 | let elemPoints = []; 29 | let elemPointsWithInaccuracy = []; 30 | elemPoints.push( 31 | [centerProj[0] - w / 2, centerProj[1] - h / 2], 32 | [centerProj[0] - w / 2, centerProj[1] + h / 2], 33 | [centerProj[0] + w / 2, centerProj[1] - h / 2], 34 | [centerProj[0] + w / 2, centerProj[1] + h / 2] 35 | ); 36 | elemPointsWithInaccuracy.push( 37 | [ 38 | elemPoints[0][0] + ri > centerProj[0] ? centerProj[0] : elemPoints[0][0] + ri, 39 | elemPoints[0][1] + ri > centerProj[1] ? centerProj[1] : elemPoints[0][1] + ri 40 | ], 41 | [ 42 | elemPoints[1][0] + ri > centerProj[0] ? centerProj[0] : elemPoints[1][0] + ri, 43 | elemPoints[1][1] - ri < centerProj[1] ? centerProj[1] : elemPoints[1][1] - ri 44 | ], 45 | [ 46 | elemPoints[2][0] - ri < centerProj[0] ? centerProj[0] : elemPoints[2][0] - ri, 47 | elemPoints[2][1] + ri > centerProj[1] ? centerProj[1] : elemPoints[2][1] + ri 48 | ], 49 | [ 50 | elemPoints[3][0] - ri < centerProj[0] ? centerProj[0] : elemPoints[3][0] - ri, 51 | elemPoints[3][1] - ri < centerProj[1] ? centerProj[1] : elemPoints[3][1] - ri 52 | ] 53 | ); 54 | return { 55 | normal: elemPoints, 56 | withInaccuracy: elemPointsWithInaccuracy 57 | }; 58 | } 59 | 60 | function checkIsInside(map, coords, elemPoints, zoom) { 61 | for (let i = 0; i < elemPoints.length; i++) { 62 | const point = map.options.get('projection').fromGlobalPixels(elemPoints[i], zoom); 63 | if (isInside(point, coords) !== 'INSIDE') { 64 | return false; 65 | } 66 | } 67 | return true; 68 | } 69 | -------------------------------------------------------------------------------- /src/polylabel/PolylabelBase.js: -------------------------------------------------------------------------------- 1 | import templateFiltersStorage from 'api/template.filtersStorage'; 2 | 3 | import CONFIG from '../config'; 4 | import dotColorFilterStorage from '../util/templateFilterStorage/dotColor'; 5 | 6 | export default class PolylabelBased { 7 | constructor(map) { 8 | this._map = map; 9 | templateFiltersStorage.add('dot-color', dotColorFilterStorage); 10 | } 11 | 12 | getPolylabelType() { 13 | return this._polylabelType; 14 | } 15 | 16 | initMapListeners(callback) { 17 | this._mapCallback = callback; 18 | this._map.events.add(['boundschange', 'actionbegin', 'actionend'], this._mapEventsHandler, this); 19 | } 20 | 21 | destroyMapListeners() { 22 | this._map.events.remove(['boundschange', 'actionbegin', 'actionend'], this._mapEventsHandler, this); 23 | } 24 | 25 | _mapEventsHandler(event) { 26 | switch (event.get('type')) { 27 | case 'actionbegin': { 28 | const action = event.originalEvent.action; 29 | action.events.add('tick', this._actionTickHandler, this); 30 | break; 31 | } 32 | case 'actionend': { 33 | const action = event.originalEvent.action; 34 | action.events.remove('tick', this._actionTickHandler, this); 35 | 36 | if (this._zoomed) { 37 | this._mapCallback('actionendzoomchange'); 38 | this._zoomed = false; 39 | } 40 | break; 41 | } 42 | } 43 | } 44 | 45 | _actionTickHandler(event) { 46 | const tick = event.get('tick'); 47 | if (tick.zoom !== this._map.getZoom()) { 48 | if (!this._zoomed) this._mapCallback('actionbeginzoomchange'); 49 | this._zoomed = true; 50 | } 51 | } 52 | 53 | /** 54 | * Returns all polygon options 55 | */ 56 | getOptions(polygon) { 57 | if (this.getPolylabelType() === 'collection') { 58 | return polygon.options.getAll(); 59 | } else { 60 | return polygon.options; 61 | } 62 | } 63 | 64 | /** 65 | * Returns the options required to create a label 66 | */ 67 | getConfigOptions(polygon) { 68 | return CONFIG.options.reduce((result, key) => { 69 | result[key] = this.getPolylabelType() === 'collection' ? polygon.options.get(key) : polygon.options[key]; 70 | return result; 71 | }, {}); 72 | } 73 | 74 | /** 75 | * Returns options (type zoomRange) that are required to create a label 76 | */ 77 | getConfigZoomRangeOptions(polygon) { 78 | return CONFIG.zoomRangeOptions.reduce((result, key) => { 79 | result[key] = this.getPolylabelType() === 'collection' ? polygon.options.get(key) : polygon.options[key]; 80 | return result; 81 | }, {}); 82 | } 83 | 84 | /** 85 | * Returns the properties that are required to create a label 86 | */ 87 | getConfigProperties(polygon) { 88 | return CONFIG.properties.reduce((result, key) => { 89 | result[key] = this.getPolylabelType() === 'collection' ? 90 | polygon.properties.get(key) : 91 | polygon.properties[key]; 92 | return result; 93 | }, {}); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docs/example-text/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Пример 6 | 7 | 9 | 11 | 15 | 17 | 32 | 102 | 103 | 104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /docs/example-img/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Пример 6 | 7 | 8 | 9 | 13 | 14 | 29 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/label/LabelBase.js: -------------------------------------------------------------------------------- 1 | import createLayoutTemplates from './util/layoutTemplates/createLayoutTemplates'; 2 | import LabelData from './util/LabelData'; 3 | import classHelper from './util/classHelper'; 4 | import getLayoutSize from './util/getLayoutSize'; 5 | 6 | export default class LabelBase { 7 | constructor() { 8 | this._baseLayoutTemplates = null; 9 | this._layoutTemplates = null; 10 | } 11 | 12 | getPolylabelType() { 13 | return this._polylabel.getPolylabelType(); 14 | } 15 | 16 | getPlacemark(type) { 17 | return this._placemark[type]; 18 | } 19 | 20 | getLayout(type) { 21 | return this._layout[type]; 22 | } 23 | 24 | setLayout(type, layout) { 25 | this._layout[type] = layout; 26 | this._initLayoutSizeChangeHandler(layout); 27 | } 28 | 29 | updateLayouts() { 30 | this.getLabelLayout('label').then(layout => { 31 | this.setLayout('label', layout); 32 | }); 33 | this.getLabelLayout('dot').then(layout => { 34 | this.setLayout('dot', layout); 35 | }); 36 | } 37 | 38 | _initLayoutSizeChangeHandler(layout) { 39 | const el = layout.getElement(); 40 | if (!el) return; 41 | let imgs = Array.prototype.slice.call(el.getElementsByTagName('img')); 42 | if (imgs.length > 0) { 43 | const imagesLoaded = Promise.all(imgs.map(img => { 44 | if (img.complete) { 45 | return Promise.resolve(); 46 | } 47 | return new Promise(resolve => img.onload = resolve); 48 | })); 49 | imagesLoaded.then(() => { 50 | const size = getLayoutSize(layout); 51 | if (!size) return; 52 | if (size.width > 0 && size.height > 0) { 53 | this._polylabel._setLabelData(this._polygon, this, undefined, ['dot', 'label']); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | getPolygonOptions() { 60 | if (this.getPolylabelType() === 'collection') { 61 | return this._polygon.options.getAll(); 62 | } 63 | return this._polygon.options; 64 | } 65 | 66 | getLabelOptions() { 67 | const label = this._placemark.label; 68 | return (this.getPolylabelType() === 'collection') ? label.options.getAll() : label.options; 69 | } 70 | 71 | /** 72 | * Forms all the necessary options for the label (from the polygon, the necessary parsed data, etc.) 73 | */ 74 | getFormedOptionsForPlacemark(type) { 75 | const cursors = this._data.getLabelCursors(); 76 | return Object.assign( 77 | {}, 78 | this.getPolygonOptions(), 79 | this._layoutTemplates[type], 80 | cursors[type] 81 | ); 82 | } 83 | 84 | /** 85 | * Create templates that will then be included in the base 86 | */ 87 | createLayoutTemplates() { 88 | const polygonOptions = this.getPolygonOptions(); 89 | const label = polygonOptions.labelLayout; 90 | const dot = polygonOptions.labelDotLayout; 91 | 92 | if (this._prevLayout && this._prevLayout.dot === dot && this._prevLayout.label === label) { 93 | return; 94 | } 95 | 96 | this._layoutTemplates = createLayoutTemplates(label, dot); 97 | 98 | this._prevLayout = { 99 | dot, 100 | label 101 | }; 102 | } 103 | 104 | /** 105 | * Returns the current visibility value 106 | */ 107 | analyseVisibility(visibleState, visible, dotVisible) { 108 | let currState = visibleState && visibleState !== 'auto' ? visibleState : visible; 109 | if (currState === 'dot' && !dotVisible) currState = 'none'; 110 | return currState; 111 | } 112 | 113 | /** 114 | * Sets visibility for a label 115 | */ 116 | setVisibility(visibleState, visible, dotVisible) { 117 | const currState = this.analyseVisibility(visibleState, visible, dotVisible); 118 | this.setVisibilityForce(currState); 119 | return currState; 120 | } 121 | 122 | createLabelData(options, zoomRangeOptions) { 123 | this._data = new LabelData(this._polygon, options, zoomRangeOptions, this._map, this); 124 | return this._data; 125 | } 126 | 127 | getLabelData() { 128 | return this._data; 129 | } 130 | 131 | analysePane(type, visibleType) { 132 | return type === visibleType ? 'places' : 'phantom'; 133 | } 134 | 135 | analyseOffset(size, offset) { 136 | const h = size.height / 2; 137 | const w = size.width / 2; 138 | 139 | return { 140 | left: -w + offset[0], 141 | top: -h + offset[1] 142 | }; 143 | } 144 | 145 | analyseStyles(data) { 146 | const labelOptions = this.getLabelOptions(); 147 | let isChange = false; 148 | Object.keys(data).forEach(key => { 149 | if (labelOptions[`iconLabel${key[0].toUpperCase()}${key.slice(1)}`] !== data[key]) { 150 | isChange = true; 151 | } 152 | }); 153 | return { 154 | isChange, 155 | styles: { 156 | iconLabelClassName: data.className, 157 | iconLabelTextSize: data.textSize, 158 | iconLabelTextColor: data.textColor 159 | } 160 | }; 161 | } 162 | 163 | analyseShape(type, size, offset) { 164 | const h = size.height / 2; 165 | const w = size.width / 2; 166 | const dotDefaultShape = (type === 'dot' && this._data.isDotDefault) ? 2 : 0; 167 | 168 | return { 169 | type: 'Rectangle', 170 | coordinates: [ 171 | [-w + offset[0] - dotDefaultShape, -h + offset[1] - dotDefaultShape], 172 | [w + offset[0] + dotDefaultShape, h + offset[1] + dotDefaultShape] 173 | ] 174 | }; 175 | } 176 | 177 | addDotClass(className) { 178 | classHelper.add(this._layout.dot, 'ymaps-polylabel-dot-default', className); 179 | } 180 | 181 | removeDotClass(className) { 182 | classHelper.remove(this._layout.dot, 'ymaps-polylabel-dot-default', className); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/label/GeoObjectCollection/Label.js: -------------------------------------------------------------------------------- 1 | import Placemark from 'api/Placemark'; 2 | import LabelPlacemarkOverlay from '../util/LabelPlacemarkOverlay'; 3 | import getBaseLayoutTemplates from '../util/layoutTemplates/getBaseLayoutTemplates'; 4 | import LabelBase from '../LabelBase'; 5 | 6 | /** 7 | * The label class for the geo-collection 8 | */ 9 | export default class Label extends LabelBase { 10 | constructor(map, polygon, parentCollection, polylabel) { 11 | super(); 12 | 13 | this._map = map; 14 | this._polylabel = polylabel; 15 | this._polygon = polygon; 16 | this._parentCollection = parentCollection; 17 | this._placemark = { 18 | label: null, 19 | dot: null 20 | }; 21 | this._layout = { 22 | label: null, 23 | dot: null 24 | }; 25 | this._init(); 26 | } 27 | 28 | removeFromCollection() { 29 | if (!this._parentCollection) { 30 | return; 31 | } 32 | ['label', 'dot'].forEach(type => { 33 | if (this._parentCollection.indexOf(this._placemark[type]) === -1) { 34 | return; 35 | } 36 | this._parentCollection.remove(this._placemark[type]); 37 | }); 38 | this._polygon = null; 39 | this._parentCollection = null; 40 | this._placemark = null; 41 | this._layout = null; 42 | this._data = null; 43 | } 44 | 45 | addToCollection() { 46 | if (!this._parentCollection) { 47 | return Promise.reject(); 48 | } 49 | const layouts = ['label', 'dot'].map(type => { 50 | if (!this._placemark[type].getParent()) { 51 | this._parentCollection.add(this._placemark[type]); 52 | } 53 | 54 | return this.getLabelLayout(type).then(layout => { 55 | this.setLayout(type, layout); 56 | }); 57 | }); 58 | return Promise.all(layouts); 59 | } 60 | 61 | getLabelLayout(type) { 62 | return this._placemark[type].getOverlay().then(overlay => overlay.getLayout()); 63 | } 64 | 65 | _init() { 66 | this._baseLayoutTemplates = getBaseLayoutTemplates(); 67 | } 68 | 69 | createPlacemarks() { 70 | ['label', 'dot'].forEach(type => { 71 | this._placemark[type] = Label._createPlacemark({ 72 | properties: Object.assign({}, { 73 | polygon: this._polygon 74 | }, this._polygon.properties.getAll()), 75 | options: this.getFormedOptionsForPlacemark(type) 76 | }, this._baseLayoutTemplates[type], this._data.getCenterCoords(this._map.getZoom())); 77 | }); 78 | } 79 | 80 | static _createPlacemark(params, layout, coords) { 81 | const options = Object.assign({}, { 82 | iconLayout: layout, 83 | pointOverlay: LabelPlacemarkOverlay, 84 | iconLabelPosition: 'absolute' 85 | }, params.options); 86 | return new Placemark(coords, params.properties, options); 87 | } 88 | 89 | setDataByZoom(zoom, visibleState) { 90 | ['dot', 'label'].forEach(type => { 91 | if (type === 'label') { 92 | const styles = this._data.getStyles(zoom); 93 | this.setStyles({ 94 | className: styles.className, 95 | textSize: styles.textSize, 96 | textColor: styles.textColor 97 | }); 98 | } 99 | this.setVisibilityForce('none'); 100 | this._data.setVisible(zoom, type, this._layout[type]); 101 | }); 102 | 103 | const currentVisibleType = this.setVisibility( 104 | visibleState, 105 | this._data.getVisibility(zoom), 106 | this._data.getData('dotVisible') 107 | ); 108 | 109 | const currentCenter = this._data.getCenterCoords(zoom); 110 | if (['label', 'dot'].indexOf(currentVisibleType) !== -1 && 111 | this._data.getSize(zoom, currentVisibleType)) { 112 | this.setCoordinates(currentCenter); 113 | this.setOffsetAndIconShape( 114 | currentVisibleType, 115 | this._data.getSize(zoom, currentVisibleType), 116 | this._data.getOffset(zoom) 117 | ); 118 | } 119 | 120 | return { 121 | currentVisibleType, 122 | currentConfiguredVisibileType: this._data.getVisibility(zoom), 123 | currentCenter 124 | }; 125 | } 126 | 127 | setLayoutTemplate() { 128 | this.createLayoutTemplates(); 129 | Object.keys(this._layoutTemplates).forEach(type => { 130 | this._placemark[type].options.set(this._layoutTemplates[type]); 131 | }); 132 | } 133 | 134 | setNewOptions(newOptions) { 135 | ['dot', 'label'].forEach(type => { 136 | this._placemark[type].options.set(newOptions); 137 | }); 138 | } 139 | 140 | /** 141 | * Sets the coordinates for the label 142 | */ 143 | setCoordinates(coords) { 144 | if (coords.toString() !== this._placemark.label.geometry.getCoordinates().toString()) { 145 | ['dot', 'label'].forEach(type => { 146 | this._placemark[type].geometry.setCoordinates(coords); 147 | }); 148 | } 149 | } 150 | 151 | setVisibilityForce(visibleType) { 152 | Object.keys(this._placemark).forEach(type => { 153 | const pane = this.analysePane(type, visibleType); 154 | if (this._placemark[type].options.get('pane') !== pane) { 155 | this._placemark[type].options.set({pane}); 156 | } 157 | }); 158 | } 159 | 160 | /** 161 | * Sets the styles for the label 162 | */ 163 | setStyles(data) { 164 | const styles = this.analyseStyles(data); 165 | if (styles.isChange) { 166 | this._placemark.label.options.set(styles.styles); 167 | } 168 | } 169 | 170 | /** 171 | * Centers the label and creates the correct iconShape 172 | */ 173 | setOffsetAndIconShape(type, size, offset) { 174 | const offsetResult = this.analyseOffset(size, offset); 175 | this._placemark[type].options.set({ 176 | iconShape: this.analyseShape(type, size, offset), 177 | iconLabelLeft: offsetResult.left, 178 | iconLabelTop: offsetResult.top 179 | }); 180 | } 181 | 182 | destroy() { 183 | this.removeFromCollection(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/label/ObjectManager/Label.js: -------------------------------------------------------------------------------- 1 | import LabelPlacemarkOverlay from '../util/LabelPlacemarkOverlay'; 2 | import getBaseLayoutTemplates from '../util/layoutTemplates/getBaseLayoutTemplates'; 3 | import LabelBase from '../LabelBase'; 4 | 5 | /** 6 | * The label class for the ObjectManager 7 | */ 8 | export default class Label extends LabelBase { 9 | constructor(map, polygon, objectManager, polylabel) { 10 | super(); 11 | this._map = map; 12 | this._polylabel = polylabel; 13 | this._polygon = polygon; 14 | this._objectManager = objectManager; 15 | this._placemark = { 16 | label: null, 17 | dot: null 18 | }; 19 | this._layout = { 20 | label: null, 21 | dot: null 22 | }; 23 | this._init(); 24 | } 25 | 26 | destroy() { 27 | this.removeFromObjectManager(); 28 | } 29 | 30 | addToObjectManager() { 31 | this.setLayoutTemplate(); 32 | ['label', 'dot'].forEach(type => { 33 | this._objectManager.add(this._placemark[type]); 34 | }); 35 | } 36 | 37 | removeFromObjectManager() { 38 | ['label', 'dot'].forEach(type => { 39 | this._objectManager.remove(this._placemark[type]); 40 | }); 41 | this._polygon = null; 42 | this._objectManager = null; 43 | this._placemark = null; 44 | this._layout = null; 45 | this._data = null; 46 | } 47 | 48 | _init() { 49 | this._baseLayoutTemplates = getBaseLayoutTemplates(); 50 | } 51 | 52 | getLabelLayout(type) { 53 | const overlay = this._objectManager.objects.overlays.getById(this._placemark[type].id); 54 | if (!overlay) return Promise.reject(); 55 | return overlay.getLayout(); 56 | } 57 | 58 | createPlacemarks() { 59 | ['label', 'dot'].forEach(type => { 60 | this._placemark[type] = Label._createPlacemark(`${type}#${this._polygon.id}`, { 61 | properties: Object.assign({}, { 62 | polygon: this._polygon 63 | }, this._polygon.properties), 64 | options: this.getFormedOptionsForPlacemark(type) 65 | }, this._baseLayoutTemplates[type], this._data.getCenterCoords(this._map.getZoom())); 66 | }); 67 | } 68 | 69 | static _createPlacemark(id, params, layout, coordinates) { 70 | const options = Object.assign({}, { 71 | iconLayout: layout, 72 | iconLabelPosition: 'absolute', 73 | overlay: LabelPlacemarkOverlay, 74 | pane: 'phantom' 75 | }, params.options); 76 | return { 77 | type: 'Feature', 78 | id, 79 | options, 80 | properties: params.properties, 81 | geometry: { 82 | type: 'Point', 83 | coordinates 84 | } 85 | }; 86 | } 87 | 88 | _updateOptions(id, options) { 89 | this._objectManager.objects.setObjectOptions(id, options); 90 | } 91 | 92 | setLayoutTemplate() { 93 | Object.keys(this._layoutTemplates).forEach(type => { 94 | this._updateOptions(this._placemark[type].id, this._layoutTemplates[type]); 95 | }); 96 | } 97 | 98 | /** 99 | * Updates all possible options in the label 100 | */ 101 | updateOptions() { 102 | ['dot', 'label'].forEach(type => { 103 | Object.assign(this._placemark[type].options, this.getFormedOptionsForPlacemark(type)); 104 | this._updateOptions( 105 | this._placemark[type].id, 106 | this._placemark[type].options 107 | ); 108 | }); 109 | } 110 | 111 | setDataByZoom(zoom, types, visibleState) { 112 | types.forEach(type => { 113 | if (type === 'label') { 114 | const styles = this._data.getStyles(zoom); 115 | this.setStyles({ 116 | className: styles.className, 117 | textSize: styles.textSize, 118 | textColor: styles.textColor 119 | }); 120 | } 121 | this.setVisibilityForce('none'); 122 | this._data.setVisible(zoom, type, this._layout[type]); 123 | }); 124 | 125 | const currentVisibleType = this.setVisibility( 126 | visibleState, 127 | this._data.getVisibility(zoom), 128 | this._data.getData('dotVisible') 129 | ); 130 | 131 | const currentCenter = this._data.getCenterCoords(zoom); 132 | if (['label', 'dot'].indexOf(currentVisibleType) !== -1 && 133 | this._data.getSize(zoom, currentVisibleType)) { 134 | this.setCoordinates(currentCenter); 135 | this.setOffsetAndIconShape( 136 | currentVisibleType, 137 | this._data.getSize(zoom, currentVisibleType), 138 | this._data.getOffset(zoom) 139 | ); 140 | } 141 | 142 | return { 143 | currentVisibleType, 144 | currentConfiguredVisibileType: this._data.getVisibility(zoom), 145 | currentCenter 146 | }; 147 | } 148 | 149 | setOffsetAndIconShape(type, size, offset) { 150 | const offsetResult = this.analyseOffset(size, offset); 151 | this._updateOptions(this._placemark[type].id, { 152 | iconShape: this.analyseShape(type, size, offset), 153 | iconLabelLeft: offsetResult.left, 154 | iconLabelTop: offsetResult.top 155 | }); 156 | } 157 | 158 | setCoordinates(coords) { 159 | if (coords.toString() !== this._placemark.label.geometry.coordinates.toString()) { 160 | ['dot', 'label'].forEach(type => { 161 | this._objectManager.remove(this._placemark[type]); 162 | this._generateNewPlacemark(type); 163 | this._placemark[type].geometry.coordinates = coords; 164 | this._objectManager.add(this._placemark[type]); 165 | }); 166 | } 167 | } 168 | 169 | setStyles(data) { 170 | const styles = this.analyseStyles(data); 171 | if (styles.isChange) { 172 | this._updateOptions(this._placemark.label.id, styles.styles); 173 | } 174 | } 175 | 176 | setVisibilityForce(visibleType) { 177 | Object.keys(this._placemark).forEach(type => { 178 | const pane = this.analysePane(type, visibleType); 179 | const label = this._objectManager.objects.getById(this._placemark[type].id); 180 | if (label && label.options.pane !== pane) { 181 | this._updateOptions(this._placemark[type].id, {pane}); 182 | } 183 | }); 184 | } 185 | 186 | _generateNewPlacemark(type) { 187 | this._placemark[type] = Object.assign({}, this._placemark[type]); 188 | const id = this._placemark[type].id; 189 | this._placemark[type].id = id[0] === '_' ? id.slice(1) : `_${id}`; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/util/getPolesOfInaccessibility.js: -------------------------------------------------------------------------------- 1 | export default function (polygonCoords, precision, debug) { 2 | return getPolesOfInaccessibility([polygonCoords], precision, debug); 3 | } 4 | 5 | function TinyQueue(data, compare) { 6 | if (!(this instanceof TinyQueue)) return new TinyQueue(data, compare); 7 | 8 | this.data = data || []; 9 | this.length = this.data.length; 10 | this.compare = compare || defaultCompare; 11 | 12 | if (this.length > 0) { 13 | for (var i = (this.length >> 1); i >= 0; i--) this._down(i); 14 | } 15 | } 16 | 17 | function defaultCompare(a, b) { 18 | return a < b ? -1 : a > b ? 1 : 0; 19 | } 20 | 21 | TinyQueue.prototype = { 22 | 23 | push: function (item) { 24 | this.data.push(item); 25 | this.length++; 26 | this._up(this.length - 1); 27 | }, 28 | 29 | pop: function () { 30 | if (this.length === 0) return undefined; 31 | 32 | var top = this.data[0]; 33 | this.length--; 34 | 35 | if (this.length > 0) { 36 | this.data[0] = this.data[this.length]; 37 | this._down(0); 38 | } 39 | this.data.pop(); 40 | 41 | return top; 42 | }, 43 | 44 | peek: function () { 45 | return this.data[0]; 46 | }, 47 | 48 | _up: function (pos) { 49 | var data = this.data; 50 | var compare = this.compare; 51 | var item = data[pos]; 52 | 53 | while (pos > 0) { 54 | var parent = (pos - 1) >> 1; 55 | var current = data[parent]; 56 | if (compare(item, current) >= 0) break; 57 | data[pos] = current; 58 | pos = parent; 59 | } 60 | 61 | data[pos] = item; 62 | }, 63 | 64 | _down: function (pos) { 65 | var data = this.data; 66 | var compare = this.compare; 67 | var halfLength = this.length >> 1; 68 | var item = data[pos]; 69 | 70 | while (pos < halfLength) { 71 | var left = (pos << 1) + 1; 72 | var right = left + 1; 73 | var best = data[left]; 74 | 75 | if (right < this.length && compare(data[right], best) < 0) { 76 | left = right; 77 | best = data[right]; 78 | } 79 | if (compare(best, item) >= 0) break; 80 | 81 | data[pos] = best; 82 | pos = left; 83 | } 84 | 85 | data[pos] = item; 86 | } 87 | }; 88 | 89 | var Queue = TinyQueue; 90 | 91 | function getPolesOfInaccessibility(polygon, precision, debug) { 92 | precision = precision || 1.0; 93 | 94 | // find the bounding box of the outer ring 95 | var minX; 96 | var minY; 97 | var maxX; 98 | var maxY; 99 | 100 | for (let i = 0; i < polygon[0].length; i++) { 101 | const p = polygon[0][i]; 102 | if (!i || p[0] < minX) minX = p[0]; 103 | if (!i || p[1] < minY) minY = p[1]; 104 | if (!i || p[0] > maxX) maxX = p[0]; 105 | if (!i || p[1] > maxY) maxY = p[1]; 106 | } 107 | 108 | var width = maxX - minX; 109 | var height = maxY - minY; 110 | var cellSize = Math.min(width, height); 111 | var h = cellSize / 2; 112 | 113 | // a priority queue of cells in order of their "potential" (max distance to polygon) 114 | var cellQueue = new Queue(null, compareMax); 115 | 116 | if (cellSize === 0) return [minX, minY]; 117 | 118 | // cover polygon with initial cells 119 | for (var x = minX; x < maxX; x += cellSize) { 120 | for (var y = minY; y < maxY; y += cellSize) { 121 | cellQueue.push(new Cell(x + h, y + h, h, polygon)); 122 | } 123 | } 124 | 125 | // take centroid as the first best guess 126 | var bestCell = getCentroidCell(polygon); 127 | 128 | // special case for rectangular polygons 129 | var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon); 130 | if (bboxCell.d > bestCell.d) bestCell = bboxCell; 131 | 132 | var numProbes = cellQueue.length; 133 | 134 | while (cellQueue.length) { 135 | // pick the most promising cell from the queue 136 | var cell = cellQueue.pop(); 137 | 138 | // update the best cell if we found a better one 139 | if (cell.d > bestCell.d) { 140 | bestCell = cell; 141 | if (debug) console.error('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes); 142 | } 143 | 144 | // do not drill down further if there's no chance of a better solution 145 | if (cell.max - bestCell.d <= precision) continue; 146 | 147 | // split the cell into four cells 148 | h = cell.h / 2; 149 | cellQueue.push(new Cell(cell.x - h, cell.y - h, h, polygon)); 150 | cellQueue.push(new Cell(cell.x + h, cell.y - h, h, polygon)); 151 | cellQueue.push(new Cell(cell.x - h, cell.y + h, h, polygon)); 152 | cellQueue.push(new Cell(cell.x + h, cell.y + h, h, polygon)); 153 | numProbes += 4; 154 | } 155 | 156 | if (debug) { 157 | console.error('num probes: ' + numProbes); 158 | console.error('best distance: ' + bestCell.d); 159 | } 160 | 161 | return [bestCell.x, bestCell.y]; 162 | } 163 | 164 | function compareMax(a, b) { 165 | return b.max - a.max; 166 | } 167 | 168 | function Cell(x, y, h, polygon) { 169 | this.x = x; // cell center x 170 | this.y = y; // cell center y 171 | this.h = h; // half the cell size 172 | this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon 173 | this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell 174 | } 175 | 176 | // signed distance from point to polygon outline (negative if point is outside) 177 | function pointToPolygonDist(x, y, polygon) { 178 | var inside = false; 179 | var minDistSq = Infinity; 180 | 181 | for (var k = 0; k < polygon.length; k++) { 182 | var ring = polygon[k]; 183 | 184 | for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) { 185 | var a = ring[i]; 186 | var b = ring[j]; 187 | 188 | if ((a[1] > y !== b[1] > y) && 189 | (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside; 190 | 191 | minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b)); 192 | } 193 | } 194 | 195 | return (inside ? 1 : -1) * Math.sqrt(minDistSq); 196 | } 197 | 198 | // get polygon centroid 199 | function getCentroidCell(polygon) { 200 | var area = 0; 201 | var x = 0; 202 | var y = 0; 203 | var points = polygon[0]; 204 | 205 | for (var i = 0, len = points.length, j = len - 1; i < len; j = i++) { 206 | var a = points[i]; 207 | var b = points[j]; 208 | var f = a[0] * b[1] - b[0] * a[1]; 209 | x += (a[0] + b[0]) * f; 210 | y += (a[1] + b[1]) * f; 211 | area += f * 3; 212 | } 213 | if (area === 0) return new Cell(points[0][0], points[0][1], 0, polygon); 214 | return new Cell(x / area, y / area, 0, polygon); 215 | } 216 | 217 | // get squared distance from a point to a segment 218 | function getSegDistSq(px, py, a, b) { 219 | var x = a[0]; 220 | var y = a[1]; 221 | var dx = b[0] - x; 222 | var dy = b[1] - y; 223 | 224 | if (dx !== 0 || dy !== 0) { 225 | var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy); 226 | 227 | if (t > 1) { 228 | x = b[0]; 229 | y = b[1]; 230 | } else if (t > 0) { 231 | x += dx * t; 232 | y += dy * t; 233 | } 234 | } 235 | 236 | dx = px - x; 237 | dy = py - y; 238 | 239 | return dx * dx + dy * dy; 240 | } 241 | -------------------------------------------------------------------------------- /src/label/util/LabelData.js: -------------------------------------------------------------------------------- 1 | import CONFIG from '../../config'; 2 | import parseZoomData from '../../util/zoom/parseZoomData'; 3 | import getPolylabelCenter from '../../util/getPolesOfInaccessibility'; 4 | import setZoomVisibility from '../../util/zoom.setZoomVisibility'; 5 | import transformPolygonCoords from '../../util/transformPolygonCoords'; 6 | import getPolygonWithMaxArea from '../../util/getPolygonWithMaxArea'; 7 | import transformHexToRGB from '../../util/transformHexToRGB'; 8 | 9 | const { 10 | MIN_ZOOM, 11 | MAX_ZOOM, 12 | DEFAULT_POLYGON_FILL_COLOR 13 | } = CONFIG; 14 | 15 | const LABEL_CLASS_DEFAULT = 'ymaps-polylabel-label-default'; 16 | const LABEL_CLASS_LIGHT = 'ymaps-polylabel-light-label'; 17 | const LABEL_CLASS_DARK = 'ymaps-polylabel-dark-label'; 18 | const LABEL_SIZE_DEFAULT = parseZoomData({'1_6': 12, '7_18': 14}); 19 | 20 | export default class LabelData { 21 | constructor(polygon, options, zoomRangeOptions, map, label) { 22 | this._map = map; 23 | this._label = label; 24 | this._polygon = polygon; 25 | this._data = { 26 | zoomInfo: {}, // Object with information for each zoom 27 | autoCenter: [0, 0], 28 | dotVisible: typeof options.labelDotVisible !== 'boolean' ? true : options.labelDotVisible 29 | }; 30 | this.parsedOptions = LabelData._parseOptions(zoomRangeOptions); 31 | this._polygonCoordsWithMaxArea = transformPolygonCoords.polygon(getPolygonWithMaxArea(this.getPolygonCoords())); 32 | this.updateDotDefaultFlag(); 33 | this._init(); 34 | } 35 | 36 | setData(key, val) { 37 | this._data[key] = val; 38 | } 39 | 40 | getData(key) { 41 | return this._data[key]; 42 | } 43 | 44 | setZoomInfo(zoom, key, value) { 45 | zoom = LabelData.zoomRound(zoom); 46 | this._data.zoomInfo[zoom][key] = value; 47 | } 48 | 49 | getZoomInfo(zoom) { 50 | zoom = LabelData.zoomRound(zoom); 51 | if (zoom || typeof zoom === 'number' && zoom === 0) { 52 | return this._data.zoomInfo[zoom]; 53 | } 54 | return this._data.zoomInfo; 55 | } 56 | 57 | getPolygonCoords() { 58 | return this._label.getPolylabelType() === 'collection' ? 59 | this._polygon.geometry.getCoordinates() : 60 | this._polygon.geometry.coordinates; 61 | } 62 | 63 | getLabelCursors() { 64 | const DEFAULT = 'grab'; 65 | const result = { 66 | dot: {}, 67 | label: {} 68 | }; 69 | if (this._label.getPolylabelType() === 'collection') { 70 | result.label.cursor = this._polygon.options.get('labelCursor') || DEFAULT; 71 | result.dot.cursor = this._polygon.options.get('labelDotCursor') || DEFAULT; 72 | } else { 73 | result.label.cursor = this._polygon.options.labelCursor || DEFAULT; 74 | result.dot.cursor = this._polygon.options.labelDotCursor || DEFAULT; 75 | } 76 | return result; 77 | } 78 | 79 | getLabelDefaults(zoom) { 80 | const defaults = this._label.getPolylabelType() === 'collection' ? 81 | this._polygon.options.get('labelDefaults') : 82 | this._polygon.options.labelDefaults; 83 | if (!defaults) return; 84 | return { 85 | className: defaults === 'dark' ? 86 | `${LABEL_CLASS_DEFAULT} ${LABEL_CLASS_DARK}` : 87 | `${LABEL_CLASS_DEFAULT} ${LABEL_CLASS_LIGHT}`, 88 | textSize: LABEL_SIZE_DEFAULT[zoom] 89 | }; 90 | } 91 | 92 | getCenterCoords(zoom) { 93 | zoom = LabelData.zoomRound(zoom); 94 | return this.parsedOptions.labelCenterCoords && 95 | this.parsedOptions.labelCenterCoords[zoom] || this._data.autoCenter; 96 | } 97 | 98 | getStyles(zoom) { 99 | zoom = LabelData.zoomRound(zoom); 100 | const {className: defaultClassName, textSize: defaultTextSize} = this.getLabelDefaults(zoom) || {}; 101 | 102 | return { 103 | className: this.parsedOptions.labelClassName && this.parsedOptions.labelClassName[zoom] || 104 | defaultClassName, 105 | textSize: this.parsedOptions.labelTextSize && this.parsedOptions.labelTextSize[zoom] || 106 | defaultTextSize, 107 | textColor: this.parsedOptions.labelTextColor && this.parsedOptions.labelTextColor[zoom] 108 | }; 109 | } 110 | 111 | getPolygonFillColor() { 112 | const color = this._label.getPolylabelType() === 'collection' ? 113 | this._polygon.options.get('fillColor') : 114 | this._polygon.options.fillColor; 115 | return color || DEFAULT_POLYGON_FILL_COLOR; 116 | } 117 | 118 | getDotColorByPolygonColor() { 119 | let color = this.getPolygonFillColor(); 120 | let checkColor = transformHexToRGB(color, 0.9); 121 | if (checkColor) color = checkColor; 122 | return color; 123 | } 124 | 125 | getVisibility(zoom) { 126 | zoom = LabelData.zoomRound(zoom); 127 | return this.parsedOptions.labelForceVisible && this.parsedOptions.labelForceVisible[zoom] || 128 | this._data.zoomInfo[zoom].visible; 129 | } 130 | 131 | getOffset(zoom) { 132 | zoom = LabelData.zoomRound(zoom); 133 | return this.parsedOptions.labelOffset && this.parsedOptions.labelOffset[zoom] || [0, 0]; 134 | } 135 | 136 | updateDotDefaultFlag() { 137 | this.isDotDefault = this._label.getPolygonOptions().labelDotLayout ? false : true; 138 | } 139 | 140 | getPermissibleInaccuracyOfVisibility(zoom) { 141 | zoom = LabelData.zoomRound(zoom); 142 | return this.parsedOptions.labelPermissibleInaccuracyOfVisibility && 143 | this.parsedOptions.labelPermissibleInaccuracyOfVisibility[zoom] || 0; 144 | } 145 | 146 | getSize(zoom, type) { 147 | zoom = LabelData.zoomRound(zoom); 148 | return this._data.zoomInfo[zoom][`${type}Size`]; 149 | } 150 | 151 | setSize(zoom, type, size) { 152 | zoom = LabelData.zoomRound(zoom); 153 | if (size.height > 0 && size.width > 0) { 154 | this._data.zoomInfo[zoom][`${type}Size`] = size; 155 | } 156 | } 157 | 158 | static zoomRound(z) { 159 | return Math.round(z); 160 | } 161 | 162 | setVisible(zoom, type, layout) { 163 | zoom = LabelData.zoomRound(zoom); 164 | if (this.getSize(zoom, type)) return; 165 | const zoomData = setZoomVisibility( 166 | type, 167 | this._map, 168 | zoom, 169 | this._data.zoomInfo[zoom].visible, 170 | layout, 171 | transformPolygonCoords.point(this.getCenterCoords(zoom), this._polygonCoordsWithMaxArea.isPositivePart), 172 | this._polygonCoordsWithMaxArea.coords, 173 | this.getOffset(zoom), 174 | this.getPermissibleInaccuracyOfVisibility(zoom) 175 | ); 176 | if (!zoomData) return; 177 | 178 | this._data.zoomInfo[zoom].visible = zoomData.visible; 179 | this.setSize(zoom, type, zoomData.size); 180 | } 181 | 182 | _checkParams() { 183 | if (!this._label.getPolygonOptions().labelLayout) { 184 | const errText = 'Не указан шаблон для подписи (labelLayout)'; 185 | console.error(errText); 186 | throw new Error(errText); 187 | } 188 | } 189 | 190 | _init() { 191 | this._checkParams(); 192 | const autoCenter = getPolylabelCenter(this._polygonCoordsWithMaxArea.coords, 1.0); 193 | this._data.autoCenter = autoCenter; 194 | 195 | for (let z = MIN_ZOOM; z <= MAX_ZOOM; z++) { 196 | this._data.zoomInfo[z] = LabelData._createDefaultZoomInfo(z); 197 | } 198 | } 199 | 200 | static _parseOptions(options) { 201 | let result = {}; 202 | Object.keys(options).forEach(key => { 203 | result[key] = parseZoomData(options[key]); 204 | }); 205 | return result; 206 | } 207 | 208 | static _createDefaultZoomInfo() { 209 | return { 210 | visible: 'none', // label | dot | none, 211 | dotSize: undefined, 212 | labelSize: undefined 213 | }; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/polylabel/PolylabelCollection.js: -------------------------------------------------------------------------------- 1 | import GeoObjectCollection from 'api/GeoObjectCollection'; 2 | import Monitor from 'api/Monitor'; 3 | import nextTick from 'api/system.nextTick'; 4 | import EventManager from 'api/event.Manager'; 5 | import Event from 'api/Event'; 6 | 7 | import PBase from './PolylabelBase'; 8 | import Label from '../label/GeoObjectCollection/Label'; 9 | import State from '../util/State'; 10 | 11 | export default class PolylabelCollection extends PBase { 12 | constructor(map, polygonsCollection) { 13 | super(map); 14 | 15 | this._map = map; 16 | this._labelsCollection = new GeoObjectCollection(); 17 | this._labelsState = new State(); 18 | this._userState = new State(); // everything that needs to be added to the user 19 | this._polygonsCollection = polygonsCollection; 20 | this._isPolygonParentChange = new WeakMap(); 21 | this._polylabelType = 'collection'; 22 | this._init(); 23 | } 24 | 25 | destroy() { 26 | this._deleteLabelStateListeners(); 27 | this._deletePolygonsListeners(); 28 | this._deletePolygonCollectionListeners(); 29 | this._deleteLabelCollection(); 30 | this._map.geoObjects.remove(this._labelsCollection); 31 | } 32 | 33 | /** 34 | * Returns the status of the label for the specified polygon 35 | */ 36 | getLabelState(polygon) { 37 | return this._userState.getState(polygon); 38 | } 39 | 40 | _init() { 41 | this._map.geoObjects.add(this._labelsCollection); 42 | this._firstCalculatePolygons().then(() => { 43 | this._initMapListeners(); 44 | this._initPolygonCollectionListeners(); 45 | this._initLabelCollectionListeners(); 46 | }); 47 | } 48 | 49 | _firstCalculatePolygons() { 50 | this._clearLabelCollection(); 51 | this._polygonsCollection.each(polygon => { 52 | this._calculateNewPolygon(polygon).then(label => { 53 | this._setLabelData(polygon, label); 54 | }); 55 | }); 56 | return Promise.resolve(); 57 | } 58 | 59 | _calculatePolygons() { 60 | let promises = []; 61 | this._polygonsCollection.each(polygon => { 62 | if (polygon.geometry.getType() === 'Polygon') { 63 | const label = this._labelsState.get(polygon, 'label'); 64 | if (label) promises.push(this._setLabelData(polygon, label)); 65 | } 66 | }); 67 | return Promise.all(promises); 68 | } 69 | 70 | /** 71 | * Cleaning the collection of labels 72 | */ 73 | _clearLabelCollection() { 74 | this._labelsCollection.removeAll(); 75 | this._labelsCollection.options.set({ 76 | pane: 'phantom' 77 | }); 78 | } 79 | 80 | /** 81 | * Destroy each label from all polygons 82 | */ 83 | _deleteLabelCollection() { 84 | this._polygonsCollection.each(polygon => { 85 | const label = this._labelsState.get(polygon, 'label'); 86 | if (label) label.destroy(); 87 | }); 88 | this._clearLabelCollection(); 89 | } 90 | 91 | /** 92 | * Calculates data for the polygon label 93 | */ 94 | _calculatePolygonLabelData(polygon, isLabelCreated) { 95 | const options = this.getConfigOptions(polygon); 96 | const zoomRangeOptions = this.getConfigZoomRangeOptions(polygon); 97 | 98 | const label = (isLabelCreated) ? 99 | this._labelsState.get(polygon, 'label') : 100 | new Label(this._map, polygon, this._labelsCollection, this); 101 | 102 | label.createLabelData(options, zoomRangeOptions); 103 | label.createLayoutTemplates(); 104 | return Promise.resolve(label); 105 | } 106 | 107 | /** 108 | * Analyzes data about the label of the polygon and establishes the parameters of the label 109 | */ 110 | _setLabelData(polygon, label, visibleState) { 111 | const data = label.setDataByZoom(this._map.getZoom(), visibleState); 112 | this._setCurrentConfiguredVisibility(polygon, data.currentConfiguredVisibileType); 113 | this._setCurrentVisibility(polygon, data.currentVisibleType); 114 | this._setCurrentCenter(polygon, data.currentCenter); 115 | } 116 | 117 | _setCurrentCenter(polygon, center) { 118 | this._userState.set(polygon, 'center', center); 119 | } 120 | 121 | /** 122 | * Sets the current visibility status for the polygon (automatically calculated) 123 | */ 124 | _setCurrentConfiguredVisibility(polygon, type) { 125 | this._userState.set(polygon, 'currentConfiguredVisibility', type); 126 | } 127 | 128 | /** 129 | * Sets the current visibility status for the polygon 130 | */ 131 | _setCurrentVisibility(polygon, type) { 132 | this._userState.set(polygon, 'currentVisibility', ['dot', 'label'].indexOf(type) !== -1 ? type : 'none'); 133 | } 134 | 135 | /** 136 | * Calculates the new polygon added to the collection 137 | */ 138 | _calculateNewPolygon(polygon) { 139 | if (polygon.geometry.getType() !== 'Polygon') { 140 | return Promise.reject(); 141 | } 142 | 143 | return new Promise(resolve => { 144 | this._calculatePolygonLabelData(polygon).then(label => { 145 | this._labelsState.set(polygon, 'label', label); 146 | this._initUserStateListener(polygon); 147 | this._initPolygonListener(polygon); 148 | label.createPlacemarks(); 149 | label.addToCollection().then(() => { 150 | resolve(label); 151 | }); 152 | }); 153 | }); 154 | } 155 | 156 | /** 157 | * Clears the status of visible all label 158 | * (on new zoom it did not conflict with the calculated data, since state priority is higher) 159 | */ 160 | _clearVisibilityInLabelsState(value) { 161 | this._polygonsCollection.each(polygon => { 162 | this._userState.set(polygon, 'visible', value); 163 | }); 164 | } 165 | 166 | /** 167 | * Listener for changing the visibility state of the label at the polygon 168 | */ 169 | _initUserStateListener(polygon) { 170 | const monitor = new Monitor(this._userState.getState(polygon)); 171 | this._userState.set(polygon, '_labelMonitor', monitor); 172 | monitor.add('visible', newValue => { 173 | this._setLabelData( 174 | polygon, 175 | this._labelsState.get(polygon, 'label'), 176 | newValue 177 | ); 178 | }); 179 | } 180 | 181 | /** 182 | * Creates listener events on the polygon 183 | */ 184 | _initPolygonListener(polygon) { 185 | if (polygon.geometry.getType() === 'Polygon') { 186 | polygon.events.add(['optionschange', 'propertieschange'], this._onPolygonOptionsChangeHandler, this); 187 | polygon.events.add('parentchange', this._onPolygonParentChangeHandler, this); 188 | } 189 | } 190 | 191 | _onPolygonParentChangeHandler(event) { 192 | this._isPolygonParentChange.set(event.get('target'), true); 193 | } 194 | 195 | _onPolygonOptionsChangeHandler(event) { 196 | nextTick(() => { // тк может произойти удаление объекта и optionschange тоже дернется 197 | // сделан nextTick чтобы до этого проверить был ли parentchange, тк он происходит до optionschange 198 | const polygon = event.get('target'); 199 | const label = this._labelsState.get(polygon, 'label'); 200 | 201 | const curr = this._isPolygonParentChange.get(polygon); 202 | if (curr || !label) return; 203 | 204 | label.setVisibilityForce('none'); 205 | label.setLayoutTemplate(); 206 | 207 | this._calculatePolygonLabelData(polygon, true).then(label => { 208 | label.updateLayouts(); 209 | this._labelsState.set(polygon, 'label', label); 210 | this._setLabelData(polygon, label); 211 | }); 212 | }); 213 | } 214 | 215 | /** 216 | * Creates listener events for a collection of polygons 217 | */ 218 | _initPolygonCollectionListeners() { 219 | this._polygonsCollection.events.add(['add', 'remove'], this._polygonCollectionEventHandler, this); 220 | } 221 | 222 | _polygonCollectionEventHandler(event) { 223 | switch (event.get('type')) { 224 | case 'add': { 225 | const polygon = event.get('child'); 226 | this._calculateNewPolygon(polygon).then(label => { 227 | this._setLabelData(polygon, label); 228 | }); 229 | break; 230 | } 231 | case 'remove': { 232 | const polygon = event.get('child'); 233 | this._deleteLabelStateListener(polygon); 234 | const label = this._labelsState.get(polygon, 'label'); 235 | if (label) label.destroy(); 236 | break; 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Makes the transfer of events from the label to the corresponding polygon 243 | */ 244 | _initLabelCollectionListeners() { 245 | const controller = { 246 | onBeforeEventFiring: (events, type, event) => { 247 | if (event.get('target').options.get('pane') === 'phantom') return false; 248 | 249 | let polygon = event.get('target').properties.get('polygon'); 250 | if (!polygon) return false; 251 | 252 | if (type === 'mouseenter' || type === 'mouseleave') { 253 | const labelInst = this._labelsState.get(polygon, 'label'); 254 | if (labelInst && labelInst.getLabelData().isDotDefault) { 255 | if (type === 'mouseenter') { 256 | labelInst.addDotClass('ymaps-polylabel-dot-default_hover'); 257 | } else { 258 | labelInst.removeDotClass('ymaps-polylabel-dot-default_hover'); 259 | } 260 | } 261 | } 262 | 263 | let newEvent = new Event({ 264 | target: polygon, 265 | type: `label${type}` 266 | }, event); 267 | 268 | polygon.events.fire(`label${type}`, newEvent); 269 | return false; 270 | } 271 | }; 272 | const eventManager = new EventManager({ 273 | controllers: [controller] 274 | }); 275 | this._labelsCollection.events.setParent(eventManager); 276 | } 277 | 278 | _initMapListeners() { 279 | this.initMapListeners((type) => { 280 | if (type === 'actionendzoomchange') { 281 | this._clearVisibilityInLabelsState(); 282 | this._calculatePolygons(); 283 | } else if (type === 'actionbeginzoomchange') { 284 | this._clearVisibilityInLabelsState('none'); 285 | } 286 | }); 287 | } 288 | 289 | _deleteLabelStateListeners() { 290 | this._polygonsCollection.each(polygon => { 291 | this._deleteLabelStateListener(polygon); 292 | }); 293 | } 294 | 295 | /** 296 | * Removes the listener for changing the visibility state of the label at the polygon 297 | */ 298 | _deleteLabelStateListener(polygon) { 299 | const monitor = this._userState.get(polygon, '_labelMonitor'); 300 | if (monitor) monitor.removeAll(); 301 | } 302 | 303 | /** 304 | * Removes listener from the polygon collection 305 | */ 306 | _deletePolygonCollectionListeners() { 307 | this._polygonsCollection.events.remove(['add', 'remove'], this._polygonCollectionEventHandler, this); 308 | this.destroyMapListeners(); 309 | } 310 | 311 | _deletePolygonsListeners() { 312 | this._polygonsCollection.each((polygon) => { 313 | this._deletePolygonListener(polygon); 314 | }); 315 | } 316 | 317 | /** 318 | * Removes event listeners from a polygon 319 | */ 320 | _deletePolygonListener(polygon) { 321 | polygon.events.remove(['optionschange', 'propertieschange'], this._onPolygonOptionsChangeHandler, this); 322 | polygon.events.remove('parentchange', this._onPolygonParentChangeHandler, this); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/polylabel/PolylabelObjectManager.js: -------------------------------------------------------------------------------- 1 | import Monitor from 'api/Monitor'; 2 | import ObjectManager from 'api/ObjectManager'; 3 | import nextTick from 'api/system.nextTick'; 4 | import EventManager from 'api/event.Manager'; 5 | 6 | import PBase from './PolylabelBase'; 7 | import Label from '../label/ObjectManager/Label'; 8 | import State from '../util/State'; 9 | 10 | export default class PolylabelObjectManager extends PBase { 11 | constructor(map, objectManager) { 12 | super(map); 13 | this._map = map; 14 | this._polygonsObjectManager = objectManager; 15 | this._labelsObjectManager = new ObjectManager(); 16 | this._labelsState = new State(); 17 | this._userState = new State(); // everything that needs to be added to the user 18 | this._polylabelType = 'objectmanager'; 19 | this._init(); 20 | } 21 | 22 | destroy() { 23 | this._deleteLabelsOverlaysListeners(); 24 | this._deleteLabelStateListeners(); 25 | this._deletePolygonsListeners(); 26 | this._deletePolygonsObjectsListeners(); 27 | this._deleteLabelsOMListeners(); 28 | } 29 | 30 | /** 31 | * Returns the status of the label for the specified polygon 32 | */ 33 | getLabelState(polygon) { 34 | return this._userState.getState(polygon); 35 | } 36 | 37 | _init() { 38 | this._map.geoObjects.add(this._labelsObjectManager); 39 | this._initLabelsOverlaysListeners(); 40 | this._initPolygonsObjectsListeners(); 41 | this._initPolygonsListeners(); 42 | this._initLabelsOMListeners(); 43 | 44 | this._calculatePolygons().then(() => this._initMapListeners()); 45 | } 46 | 47 | /** 48 | * Sets the data for labels for the current zoom 49 | */ 50 | _calculatePolygons() { 51 | this._polygonsObjectManager.objects.each(polygon => { 52 | this._calculateNewPolygon(polygon); 53 | }); 54 | return Promise.resolve(); 55 | } 56 | 57 | /** 58 | * Calculates data for the label signature 59 | * Creates a label 60 | */ 61 | _calculatePolygonLabelData(polygon, isLabelCreated) { 62 | const options = this.getConfigOptions(polygon); 63 | const zoomRangeOptions = this.getConfigZoomRangeOptions(polygon); 64 | 65 | const label = (isLabelCreated) ? 66 | this._labelsState.get(polygon, 'label') : 67 | new Label(this._map, polygon, this._labelsObjectManager, this); 68 | 69 | label.createLabelData(options, zoomRangeOptions); 70 | label.createLayoutTemplates(); 71 | return Promise.resolve(label); 72 | } 73 | 74 | /** 75 | * Analyzes data about the label of the polygon and establishes the parameters of the label 76 | */ 77 | _setLabelData(polygon, label, visibleState, types) { 78 | const data = label.setDataByZoom(this._map.getZoom(), types, visibleState); 79 | this._setCurrentConfiguredVisibility(polygon, data.currentConfiguredVisibileType); 80 | this._setCurrentVisibility(polygon, data.currentVisibleType); 81 | this._setCurrentCenter(polygon, data.currentCenter); 82 | } 83 | 84 | _setCurrentCenter(polygon, center) { 85 | this._userState.set(polygon, 'center', center); 86 | } 87 | 88 | _setCurrentConfiguredVisibility(polygon, type) { 89 | this._userState.set(polygon, 'currentConfiguredVisibility', type); 90 | } 91 | 92 | _setCurrentVisibility(polygon, type) { 93 | this._userState.set(polygon, 'currentVisibility', ['dot', 'label'].indexOf(type) !== -1 ? type : 'none'); 94 | } 95 | 96 | /** 97 | * Listener for changing the visibility state of the label at the polygon 98 | */ 99 | _initLabelStateListener(polygon) { 100 | const monitor = new Monitor(this._userState.getState(polygon)); 101 | this._userState.set(polygon, '_labelMonitor', monitor); 102 | monitor.add('visible', newValue => { 103 | if (!polygon) return; 104 | this._setLabelData( 105 | polygon, 106 | this._labelsState.get(polygon, 'label'), 107 | newValue, 108 | ['dot', 'label'] 109 | ); 110 | }); 111 | } 112 | 113 | _initLabelsOverlaysListeners() { 114 | this._labelsObjectManager.objects.overlays.events.add( 115 | ['add', 'remove'], this._labelsOverlaysEventHandler, this 116 | ); 117 | } 118 | 119 | _getLabelType(labelId) { 120 | return labelId.indexOf('label#') !== -1 ? 'label' : 'dot'; 121 | } 122 | 123 | _labelOverlaysGeometryChangeHandler(event) { 124 | const overlay = event.get('target'); 125 | const labelId = overlay._data.id; 126 | 127 | overlay.getLayout().then(layout => { 128 | const labelType = this._getLabelType(labelId); 129 | const label = this._labelsObjectManager.objects.getById(labelId); 130 | if (!label) return; 131 | 132 | const polygon = label.properties.polygon; 133 | const labelInst = this._labelsState.get(polygon, 'label'); 134 | labelInst.setLayout(labelType, layout); 135 | this._setLabelData(polygon, labelInst, undefined, [labelType]); 136 | }); 137 | } 138 | 139 | _labelsOverlaysEventHandler(event) { 140 | const labelId = event.get('objectId'); 141 | const labelType = this._getLabelType(labelId); 142 | const overlay = event.get('overlay'); 143 | 144 | switch (event.get('type')) { 145 | case 'add': { 146 | overlay.events.add('geometrychange', this._labelOverlaysGeometryChangeHandler, this); 147 | nextTick(() => { // overlay добавляется на карту на следующем тике 148 | if (!overlay) return; 149 | overlay.getLayout().then(layout => { 150 | if (!layout) return; 151 | const label = this._labelsObjectManager.objects.getById(labelId); 152 | if (!label) return; 153 | 154 | const polygon = label.properties.polygon; 155 | const labelInst = this._labelsState.get(polygon, 'label'); 156 | labelInst.setLayout(labelType, layout); 157 | this._setLabelData(polygon, labelInst, undefined, [labelType]); 158 | }); 159 | }); 160 | break; 161 | } 162 | case 'remove': { 163 | overlay.events.remove('geometrychange', this._labelOverlaysGeometryChangeHandler, this); 164 | break; 165 | } 166 | } 167 | } 168 | 169 | _clearVisibilityInLabelsState(value) { 170 | this._polygonsObjectManager.objects.each(polygon => { 171 | this._userState.set(polygon, 'visible', value); 172 | }); 173 | } 174 | 175 | _initMapListeners() { 176 | this.initMapListeners(type => { 177 | if (type === 'actionendzoomchange') { 178 | this._clearVisibilityInLabelsState(); 179 | } else if (type === 'actionbeginzoomchange') { 180 | this._clearVisibilityInLabelsState('none'); 181 | } 182 | }); 183 | } 184 | 185 | _initPolygonsObjectsListeners() { 186 | this._polygonsObjectManager.objects.events.add(['add', 'remove'], this._polygonCollectionEventHandler, this); 187 | } 188 | 189 | _polygonCollectionEventHandler(event) { 190 | switch (event.get('type')) { 191 | case 'add': { 192 | const polygon = event.get('child'); 193 | this._calculateNewPolygon(polygon); 194 | break; 195 | } 196 | case 'remove': { 197 | const polygon = event.get('child'); 198 | this._deleteLabelStateListener(polygon); 199 | const label = this._labelsState.get(polygon, 'label'); 200 | if (label) label.destroy(); 201 | break; 202 | } 203 | } 204 | } 205 | 206 | _calculateNewPolygon(polygon) { 207 | if (polygon.geometry.type === 'Polygon') { 208 | this._calculatePolygonLabelData(polygon).then(label => { 209 | this._labelsState.set(polygon, 'label', label); 210 | this._initLabelStateListener(polygon); 211 | label.createPlacemarks(); 212 | label.addToObjectManager(); 213 | }); 214 | } 215 | } 216 | 217 | _initPolygonsListeners() { 218 | this._polygonsObjectManager.objects.events.add( 219 | ['optionschange', 'objectoptionschange'], 220 | this._onPolygonOptionsChangeHandler, 221 | this 222 | ); 223 | } 224 | 225 | _onPolygonOptionsChangeHandler(event) { 226 | const polygon = this._polygonsObjectManager.objects.getById(event.get('objectId')); 227 | if (!polygon) return; 228 | 229 | this._calculatePolygonLabelData(polygon, true).then(label => { 230 | label.setVisibilityForce('none'); 231 | this._labelsState.set(polygon, 'label', label); 232 | 233 | label.setLayoutTemplate(); 234 | label.updateOptions(); 235 | label.updateLayouts(); 236 | this._setLabelData(polygon, this._labelsState.get(polygon, 'label'), undefined, ['dot', 'label']); 237 | }); 238 | } 239 | 240 | _initLabelsOMListeners() { 241 | let controller = { 242 | onBeforeEventFiring: (events, type, event) => { 243 | const labelId = event.get('objectId'); 244 | if (!labelId) return false; 245 | 246 | let polygonId = labelId.split('#')[1]; 247 | polygonId = isNaN(Number(polygonId)) ? polygonId : Number(polygonId); 248 | 249 | const polygon = this._polygonsObjectManager.objects.getById(polygonId); 250 | const label = this._labelsObjectManager.objects.getById(labelId); 251 | if (label && label.options.pane === 'phantom' || !polygon) return false; 252 | 253 | if (type === 'mouseenter' || type === 'mouseleave') { 254 | const labelInst = this._labelsState.get(polygon, 'label'); 255 | if (labelInst && labelInst.getLabelData().isDotDefault) { 256 | if (type === 'mouseenter') { 257 | labelInst.addDotClass('ymaps-polylabel-dot-default_hover'); 258 | } else { 259 | labelInst.removeDotClass('ymaps-polylabel-dot-default_hover'); 260 | } 261 | } 262 | } 263 | 264 | this._polygonsObjectManager.events.fire(`label${type}`, { 265 | objectId: polygonId, 266 | type: `label${type}` 267 | }); 268 | 269 | return false; 270 | } 271 | }; 272 | let eventManager = new EventManager({ 273 | controllers: [controller] 274 | }); 275 | this._labelsObjectManager.events.setParent(eventManager); 276 | } 277 | 278 | _deleteLabelStateListeners() { 279 | this._polygonsObjectManager.objects.each(polygon => { 280 | if (polygon.geometry.type === 'Polygon') { 281 | this._deleteLabelStateListener(polygon); 282 | } 283 | }); 284 | } 285 | 286 | _deleteLabelStateListener(polygon) { 287 | const monitor = this._userState.get(polygon, '_labelMonitor'); 288 | if (monitor) monitor.removeAll(); 289 | } 290 | 291 | _deleteLabelsOverlaysListeners() { 292 | this._labelsObjectManager.objects.overlays.events.remove( 293 | ['add', 'remove'], this._labelsOverlaysEventHandler, this 294 | ); 295 | } 296 | 297 | _deletePolygonsListeners() { 298 | this._polygonsObjectManager.objects.events.remove(['optionschange', 'objectoptionschange'], 299 | this._onPolygonOptionsChangeHandler, this); 300 | } 301 | 302 | _deletePolygonsObjectsListeners() { 303 | this._polygonsObjectManager.objects.events.remove(['add', 'remove'], this._polygonCollectionEventHandler, this); 304 | this.destroyMapListeners(); 305 | } 306 | 307 | /** 308 | * Destroy each label from all polygons 309 | */ 310 | _deleteLabelsOMListeners() { 311 | this._polygonsObjectManager.objects.each(polygon => { 312 | const label = this._labelsState.get(polygon, 'label'); 313 | if (polygon.geometry.type === 'Polygon' && label) { 314 | label.destroy(); 315 | } 316 | }); 317 | this._clearLabels(); 318 | } 319 | 320 | _clearLabels() { 321 | this._labelsObjectManager.removeAll(); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yandex.Maps API Polylabeler Plugin 2 | =================== 3 | 4 | This module allows to set labels inside polygons on the map choosing the suitable position automatically. 5 | It is created for [Yandex.Maps JS API v2.1](https://tech.yandex.ru/maps/doc/jsapi/2.1/quick-start/tasks/quick-start-docpage/) and based on [Polylabel module](https://github.com/mapbox/polylabel) 6 | 7 | [Demo1](https://yandex.github.io/mapsapi-polylabeler/docs/example-text/) 8 | [Demo2](https://yandex.github.io/mapsapi-polylabeler/docs/example-img/) 9 | 10 | ![example img](https://cdn.rawgit.com/yandex/mapsapi-polylabeler/6e240004/docs/res/example1.png) 11 | 12 | Loading 13 | ============ 14 | 15 | 1. Load [Yandex.Maps JS API 2.1](https://tech.yandex.ru/maps/doc/jsapi/2.1/quick-start/tasks/quick-start-docpage/), Polylabeler Plugin and ([Area calculation plugin](https://github.com/yandex/mapsapi-area)) (It is used by the present module) by adding the following code into the **head** section of your page. 16 | 17 | ```html 18 | 19 | 20 | 21 | 22 | ``` 23 | 24 | Simple example 25 | ============ 26 | #### Example shows, how to create label for one polygon. 27 | 28 | ```js 29 | ymaps.ready(['polylabel.create']).then(function () { 30 | const map = new ymaps.Map('map', { 31 | center: [65, 81], 32 | zoom: 5 33 | }); 34 | const objectManager = new ymaps.ObjectManager(); 35 | 36 | objectManager.add({ 37 | type: 'Feature', 38 | id: 1, 39 | geometry: { 40 | type: 'Polygon', 41 | coordinates: [[ 42 | [66 , 74], 43 | [68, 92], 44 | [59, 88], 45 | [62, 80], 46 | [66, 74] 47 | ]] 48 | }, 49 | properties: { 50 | name: 'nameOfMyPolygon' 51 | }, 52 | options: { 53 | labelDefaults: 'light', 54 | labelLayout: '{{properties.name}}' 55 | } 56 | }); 57 | map.geoObjects.add(objectManager); 58 | const polylabel = new ymaps.polylabel.create(map, objectManager); 59 | }); 60 | ``` 61 | 62 | Launch 63 | ============ 64 | It is possible to work with two API entities: 65 | * [GeoObjectCollection](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/GeoObjectCollection-docpage/) 66 | * [ObjectManager](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/ObjectManager-docpage/) 67 | 68 | Get access to the module functions by using [ymaps.modules.require](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/modules.require-docpage/) method: 69 | ```js 70 | ymaps.modules.require(['polylabel.create']).then(function (Polylabel) { 71 | /** 72 | * @param {Map} map - map instance 73 | * @param {GeoObjectCollection | ObjectManager} component - 74 | * instance of a collection or an object manager that contains polygons to be labeled 75 | */ 76 | const polyLabeler = new Polylabel(map, component); 77 | }); 78 | ``` 79 | 80 | 81 | Documentation 82 | ============ 83 | The module works according to the following principle: 84 | Two labels are created for the polygon: a small (**dot**) and a main (**label**). 85 | If the main label can't fit into the polygon, the module tries to place a small one; 86 | If both does not fit, nothing is displayed. 87 | 88 | ## Methods 89 | | Name | Returns | Description | 90 | |------|---------|-------------| 91 | | [getLabelState(polygon)](#getlabelstate) | [DataManager](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/data.Manager-docpage/) | label state | 92 | 93 | ### getLabelState 94 | | Parameter | Type | Default value | Description | 95 | |-----------|------|---------------|-------------| 96 | | polygon | [GeoObject](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/GeoObject-docpage/) | - | Object, describing a polygon. Use GeoObject for the [GeoObjectCollection](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/GeoObjectCollection-docpage/) | 97 | | | JSON | - | Use JSON for the [ObjectManager](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/ObjectManager-docpage/) | 98 | 99 | 100 | ```js 101 | const polyLabeler = new Polylabel(map, objectManager); 102 | let state = polyLabeler.getLabelState(polygon); 103 | ``` 104 | 105 | ## State 106 | 107 | ### write/read 108 | | Name | Type | Default value | Value | Description | 109 | |------|------|---------------|-------|-------------| 110 | | visible | string\|undefined | undefined | dot | Show small label | 111 | | | | | label | Show main label | 112 | | | | | none | Hide all labels | 113 | | | | | undefined | Automatic calculation | 114 | 115 | With map zoom change the state resets to default. 116 | 117 | In ObjectManager version of polylabeler state resets to default on any change within labels in vieport, i.e. on any zoom and almost on any drag. It's better to set `labelForceVisible` option to hide labels instead of updating state's `visible`. 118 | 119 | ```js 120 | const polyLabeler = new Polylabel(map, objectManager); 121 | let state = polyLabeler.getLabelState(polygon); 122 | state.set('visible', 'dot'); 123 | ``` 124 | 125 | ### only read 126 | | Name | Type | Description | 127 | |------|------|-------------| 128 | | center | Array[2] | Current center of label | 129 | | currentVisibility | string | Label current visibility | 130 | | currentConfiguredVisibility | string | Label visiblity, which configured by module | 131 | 132 | ```js 133 | const polyLabeler = new Polylabel(map, objectManager); 134 | let state = polyLabeler.getLabelState(polygon); 135 | state.get('center'); 136 | ``` 137 | 138 | 139 | ## Events 140 | Labels events can be accessed through polygons. 141 | List of events can be found in 142 | [GeoObject](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/GeoObject-docpage/#events-summary). 143 | For events handling add listener for the desired event with the prefix **"label"** to 144 | [EventManager](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/event.Manager-docpage/) of [GeoObjectCollection](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/GeoObjectCollection-docpage/) or [ObjectManager](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/ObjectManager-docpage/). 145 | 146 | ```js 147 | // In this example, we listen to pointing the cursor at the label and 148 | // moving the cursor off the label 149 | geoObjectCollection.events.add(['labelmouseenter', 'labelmouseleave'], event => { 150 | // Get the polygon on which the event occured 151 | var polygon = event.get('target'); 152 | // Get label state 153 | var state = polyLabeler.getLabelState(polygon); 154 | // Change the visibility of the label depending on the type of event 155 | state.set('visible', event.get('type') === 'labelmouseleave' ? undefined : 'label'); 156 | }); 157 | ``` 158 | 159 | 160 | ## Polygon options 161 | ### The label is controlled via the polygon options 162 | **\* Mandatory option** 163 | 164 | | Name | Type | Default value | 165 | |------|------|---------------| 166 | | [labelLayout](#labellayout) * | string | - | 167 | | [labelDotLayout](#labeldotlayout) | string | default dot layout | 168 | | [labelDotVisible](#labeldotvisible) | boolean | true | 169 | | [labelDefaults](#labeldefaults) | string | - | 170 | | [labelCursor](#labelcursor) | string | 'grab' | 171 | | [labelDotCursor](#labeldotcursor) | string | 'grab' | 172 | | [labelClassName](#labelclassname) | ZoomRange < string > \|\| string | - | 173 | | [labelForceVisible](#labelforcevisible)| ZoomRange < string > \|\| string | - | 174 | | [labelTextColor](#labeltextcolor) | ZoomRange < string > \|\| string | - | 175 | | [labelTextSize](#labeltextsize) | ZoomRange < number > \|\| number | - | 176 | | [labelCenterCoords](#labelcentercoords)| ZoomRange < Array[2]< number >> \|\| Array[2]< number > | - | 177 | | [labelOffset](#labeloffset) | ZoomRange < Array[2]< number >> \|\| Array[2]< number > | \[0, 0\] | 178 | | [labelPermissibleInaccuracyOfVisibility](#labelpermissibleinaccuracyofvisibility) | ZoomRange < number > \|\| number | 0 | 179 | 180 | Type **ZoomRange< T >** object allows to set zoom level or zoom range to specify values for the certain scales. 181 | **T** - type of value.
182 | ```js 183 | // In this example we specify 'someOptions' value for different zoom levels 184 | // for the 1-st zoom it will be set to 12, 185 | // from 2-nd to 5-th the value is 14, from 6-th to 22-nd it is 16 and on the 23-rd it is 18 186 | someOptions: { 187 | 1: 12, 188 | '2_5': 14, 189 | '6_22': 16, 190 | 23: 18 191 | } 192 | // or you can specify same value for all zoom levels 193 | someOptions: 12 194 | // if you skip some zoom levels, they will have a value calculated automatically or they will be simply ignored 195 | someOptions: { 196 | 1: 12, 197 | '2_3': 13 198 | } 199 | ``` 200 | 201 | ### labelLayout 202 | Template that describes the layout of a main label. 203 | [Based on Template](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/Template-docpage/) 204 | ```js 205 | polygon.options.set({ 206 | labelLayout: '
bar
' 207 | }); 208 | ``` 209 | 210 | ### labelDotLayout 211 | Template that describes the layout of a small label (dot). 212 | 213 | **Default behavior:** 214 | If you do not specify this option, the default dot will be drawn. 215 | 216 | ```js 217 | polygon.options.set({ 218 | labelDotLayout: `
` 220 | }); 221 | ``` 222 | 223 | ### labelDotVisible 224 | Responsible for small labels displaying. 225 | - ***true*** - show 226 | - ***false*** - don't show 227 | 228 | **Default behavior:** 229 | Small labels are displayed. 230 | 231 | ### labelDefaults 232 | Responsible for the labels default layout 233 | 234 | ***Values:*** 235 | - ***dark*** - dark theme 236 | - ***light*** - light theme 237 | 238 | ### labelCursor 239 | Cursor type to be displayed when hovering over the main label. 240 | [Possible Values](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/util.cursor.Manager-docpage/#push-param-key) 241 | 242 | ### labelDotCursor 243 | Cursor type to be displayed when hovering over the small label. 244 | [Possible Values](https://tech.yandex.ru/maps/doc/jsapi/2.1/ref/reference/util.cursor.Manager-docpage/#push-param-key) 245 | 246 | ### labelClassName 247 | CSS-class name to be applied to the label. 248 | **Important:** if you use *labelDefaults* option, *labelClassName* doesn't work. 249 | 250 | ```js 251 | // Apply "foo-class" to the label on the first zoom level, 252 | // for others apply 'bar-class" 253 | 254 | polygon.options.set({ 255 | labelClassName: { 256 | 1: 'foo-class', 257 | '2_23': 'bar-class' 258 | } 259 | }); 260 | ``` 261 | 262 | ### labelForceVisible 263 | Label type to be displayed. 264 | - ***label*** - show main label 265 | - ***dot*** - show small label 266 | - ***none*** - don't display anything 267 | 268 | ```js 269 | // On the first two zoom levels, the main label will be always shown. 270 | // On the others - small label will be always shown. 271 | 272 | polygon.options.set({ 273 | labelForceVisible: { 274 | '0_1': 'label', 275 | '2_23': 'dot' 276 | } 277 | }); 278 | ``` 279 | 280 | **Default behavior:** 281 | Automatic calculation. 282 | 283 | ### labelTextColor 284 | Label font color. 285 | 286 | ```js 287 | //Set '#FCEA00' as labels font color for all zoom levels 288 | polygon.options.set({ 289 | labelTextColor: '#FCEA00' 290 | }); 291 | ``` 292 | 293 | ### labelTextSize 294 | Label font size. 295 | 296 | ```js 297 | // On the first five zoom levels, labels font size is '22'. 298 | // On the others - '11'. 299 | polygon.options.set({ 300 | labelTextSize: { 301 | '0_4': 22, 302 | '5_23': 11 303 | } 304 | }); 305 | ``` 306 | 307 | ### labelCenterCoords 308 | Geographic coordinates where the label will be displayed. 309 | 310 | ```js 311 | // On the first zoom level coords - [37.0192, 61.01210] 312 | // On the next two - [38.123, 62.9182] 313 | // On the others - Automatic calculation 314 | polygon.options.set({ 315 | labelCenterCoords: { 316 | 0: [37.0192, 61.01210], 317 | '2_3': [38.123, 62.9182] 318 | } 319 | }); 320 | ``` 321 | 322 | **Default behavior:** 323 | Automatic calculation. 324 | 325 | ### labelOffset 326 | Offset in pixels from the current position of the label. 327 | - zero element - left offset. 328 | - first element - top offset. 329 | 330 | ```js 331 | //Set labels offset to the left as 10px and down as 20px 332 | polygon.options.set({ 333 | labelOffset: [-10, 20] 334 | }); 335 | ``` 336 | 337 | **Default behavior:** 338 | labelOffset: \[0, 0] 339 | 340 | ### labelPermissibleInaccuracyOfVisibility 341 | The value in pixels describing max area outside of the polygon that could be occupied by the label. 342 | 343 | ```js 344 | // Allow labels to step out of the polygon for 2px at all zoom levels 345 | polygon.options.set({ 346 | labelPermissibleInaccuracyOfVisibility: 2 347 | }); 348 | ``` 349 | 350 | **Default behavior:** 351 | labelPermissibleInaccuracyOfVisibility: 0 352 | 353 | 354 | ## Used libraries 355 | [https://github.com/mapbox/polylabel](https://github.com/mapbox/polylabel) 356 | 357 | ## Third party components 358 | 359 | The views and conclusions contained in the software and documentation are those 360 | of the authors and should not be interpreted as representing official policies, 361 | either expressed or implied, of OpenLayers Contributors. 362 | --------------------------------------------------------------------------------