Skip to content

Commit

Permalink
add vuefy, fix isVisible, add max zoom for clusterisation (#3)
Browse files Browse the repository at this point in the history
* add vuefy, fix isVisible, add max zoom for clusterisation

* fix test
  • Loading branch information
matthew44-mappable authored Mar 26, 2024
1 parent b953f08 commit 9e1b57e
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/MMapClusterer/MMapClusterer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('MMapClusterer', () => {
};

clusterer.update({method});
expect(entities.length).toBe(3);
expect(entities.length).toBe(4);

map.setLocation({zoom: 3});
expect(entities.length).toBe(10);
Expand Down
38 changes: 32 additions & 6 deletions src/MMapClusterer/MMapClusterer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {ClustererObject, EntitiesMap, Feature, IClusterMethod} from './inte
import {THROTTLE_DEFAULT_TIMEOUT_MS} from './constants';
import {throttle} from './helpers/throttle';
import {MMapClustererReactifyOverride} from './react/MMapClusterer';
import {MMapClustererVuefyOptions, MMapClustererVuefyOverride} from './vue/MMapClusterer';

/**
* MMapClusterer props
Expand All @@ -22,6 +23,11 @@ type MMapClustererProps = {
tickTimeout?: number;
/** Return false, if you want to override the render */
onRender?: (clusters: ClustererObject[]) => void | false;
/**
* Maximum zoom for clusterisation.
* If map zoom is bigger, markers will be displayed as is.
**/
maxZoom?: number;
};

type DefaultProps = typeof defaultProps;
Expand Down Expand Up @@ -57,6 +63,8 @@ const defaultProps = Object.freeze({
class MMapClusterer extends mappable.MMapComplexEntity<MMapClustererProps, DefaultProps> {
static defaultProps = defaultProps;
static [mappable.overrideKeyReactify] = MMapClustererReactifyOverride;
static [mappable.overrideKeyVuefy] = MMapClustererVuefyOverride;
static [mappable.optionsKeyVuefy] = MMapClustererVuefyOptions;

/** All created entities with cluster id*/
private _entitiesCache: EntitiesMap = {};
Expand All @@ -73,7 +81,8 @@ class MMapClusterer extends mappable.MMapComplexEntity<MMapClustererProps, Defau
}

/**
* Compare feature coordinates with bounds
* Compare feature coordinates with bounds.
* Visible in x2 bounds.
*
* @param feature
* @param bounds
Expand All @@ -84,7 +93,14 @@ class MMapClusterer extends mappable.MMapComplexEntity<MMapClustererProps, Defau
const {x, y} = projection.toWorldCoordinates(feature.geometry.coordinates as LngLat);
const {x: x1, y: y1} = projection.toWorldCoordinates(bounds[0]);
const {x: x2, y: y2} = projection.toWorldCoordinates(bounds[1]);
return x1 <= x && y1 >= y && x2 >= x && y2 <= y;
const boundsWidth = x2 - x1;
const boundsHeight = y1 - y2;
return (
x1 - boundsWidth / 2 <= x &&
x2 + boundsWidth / 2 >= x &&
y1 + boundsHeight / 2 >= y &&
y2 - boundsHeight / 2 <= y
);
}

/**
Expand Down Expand Up @@ -155,10 +171,20 @@ class MMapClusterer extends mappable.MMapComplexEntity<MMapClustererProps, Defau
this._isVisible(feature, map.bounds, map.projection)
);

const nextViewportObjects = this._props.method.render({
map,
features: visibleFeatures
});
let nextViewportObjects: ClustererObject[];
if (this._props.maxZoom && map.zoom > this._props.maxZoom) {
nextViewportObjects = visibleFeatures.map((feature) => ({
world: map.projection.toWorldCoordinates(feature.geometry.coordinates),
lnglat: feature.geometry.coordinates,
clusterId: feature.id,
features: [feature]
}));
} else {
nextViewportObjects = this._props.method.render({
map,
features: visibleFeatures
});
}

if (this._props.onRender && this._props.onRender(nextViewportObjects) === false) {
return;
Expand Down
58 changes: 58 additions & 0 deletions src/MMapClusterer/vue/MMapClusterer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type TVue from '@vue/runtime-core';
import type {LngLat} from '@mappable-world/mappable-types/common/types';
import type {CustomVuefyFn, CustomVuefyOptions} from '@mappable-world/mappable-types/modules/vuefy';
import type {ClustererObject, MMapClusterer, MMapClustererProps} from '..';
import {THROTTLE_DEFAULT_TIMEOUT_MS} from '../constants';
import type {Feature} from '../interface';

export const MMapClustererVuefyOptions: CustomVuefyOptions<MMapClusterer> = {
props: {
method: {type: Object, required: true},
features: {type: Array, required: true},
marker: Function as TVue.PropType<MMapClustererProps['marker']>,
cluster: Function as TVue.PropType<MMapClustererProps['cluster']>,
tickTimeout: {type: Number, default: THROTTLE_DEFAULT_TIMEOUT_MS},
onRender: Function as TVue.PropType<MMapClustererProps['onRender']>,
maxZoom: {type: Number}
}
};

type EntitiesMap = {
[x: string]: TVue.VNodeChild;
};

type MMapClustererSlots = {
marker?: {feature: Feature};
cluster?: {coordinates: LngLat; features: Feature[]};
};

export const MMapClustererVuefyOverride: CustomVuefyFn<MMapClusterer> = (MMapClustererI, props, {vuefy, Vue}) => {
const MMapClustererV = vuefy.entity(MMapClustererI, props);
const MMapCollectionV = vuefy.entity(mappable.MMapCollection, props);
return Vue.defineComponent({
name: 'MMapClustererContainer',
props,
slots: Object as TVue.SlotsType<MMapClustererSlots>,
setup(props, {slots}) {
const clustersVNode: TVue.Ref<TVue.VNodeChild[] | null> = Vue.ref(null);
const onRender = (clusters: ClustererObject[]): false => {
const vueClusters: EntitiesMap = {};
clusters.forEach(({lnglat, features, clusterId}) => {
vueClusters[clusterId] = Vue.h(
Vue.Fragment,
{key: clusterId},
features.length === 1
? slots.marker?.({feature: features[0]})
: slots.cluster?.({coordinates: lnglat, features})
);
});
clustersVNode.value = Object.values(vueClusters);
return false;
};
return () => [
Vue.h(MMapClustererV, {...props, onRender} as MMapClustererProps),
Vue.h(MMapCollectionV, () => clustersVNode.value)
];
}
});
};

0 comments on commit 9e1b57e

Please sign in to comment.