Compare commits
2 Commits
c6abf8ad4a
...
b02e3882cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b02e3882cc | ||
|
|
c56bb57fbf |
@@ -128,6 +128,53 @@ const loadEntityIcon = async (map: MapboxMapType, type: 'offer' | 'hub' | 'suppl
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standard colors for entity types
|
||||||
|
const ENTITY_COLORS = {
|
||||||
|
hub: '#22c55e', // green
|
||||||
|
supplier: '#3b82f6', // blue
|
||||||
|
offer: '#f97316' // orange
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Load all icons for related points (each type with its standard color)
|
||||||
|
const loadRelatedPointIcons = async (map: MapboxMapType) => {
|
||||||
|
const types: Array<'hub' | 'supplier' | 'offer'> = ['hub', 'supplier', 'offer']
|
||||||
|
for (const type of types) {
|
||||||
|
const iconName = `related-icon-${type}`
|
||||||
|
if (map.hasImage(iconName)) {
|
||||||
|
map.removeImage(iconName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = createEntityIcon(type, ENTITY_COLORS[type])
|
||||||
|
const img = new Image(32, 32)
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 32
|
||||||
|
canvas.height = 32
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (ctx) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(16, 16, 15, 0, 2 * Math.PI)
|
||||||
|
ctx.fillStyle = ENTITY_COLORS[type]
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = 'white'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.drawImage(img, 4, 4, 24, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = ctx?.getImageData(0, 0, 32, 32)
|
||||||
|
if (imageData) {
|
||||||
|
map.addImage(iconName, { width: 32, height: 32, data: imageData.data })
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mapOptions = computed(() => ({
|
const mapOptions = computed(() => ({
|
||||||
style: 'mapbox://styles/mapbox/satellite-streets-v12',
|
style: 'mapbox://styles/mapbox/satellite-streets-v12',
|
||||||
center: props.initialCenter,
|
center: props.initialCenter,
|
||||||
@@ -372,27 +419,28 @@ const initClientClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Related points layer (for Info mode - colored by type)
|
// Related points layer (for Info mode - icons by type)
|
||||||
|
await loadRelatedPointIcons(map)
|
||||||
|
|
||||||
map.addSource(relatedSourceId.value, {
|
map.addSource(relatedSourceId.value, {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: relatedPointsGeoJson.value
|
data: relatedPointsGeoJson.value
|
||||||
})
|
})
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: `${props.mapId}-related-circles`,
|
id: `${props.mapId}-related-points`,
|
||||||
type: 'circle',
|
type: 'symbol',
|
||||||
source: relatedSourceId.value,
|
source: relatedSourceId.value,
|
||||||
paint: {
|
layout: {
|
||||||
'circle-radius': 8,
|
'icon-image': [
|
||||||
'circle-color': [
|
|
||||||
'match',
|
'match',
|
||||||
['get', 'type'],
|
['get', 'type'],
|
||||||
'hub', '#22c55e', // green
|
'hub', 'related-icon-hub',
|
||||||
'supplier', '#3b82f6', // blue
|
'supplier', 'related-icon-supplier',
|
||||||
'offer', '#f97316', // orange
|
'offer', 'related-icon-offer',
|
||||||
'#06b6d4' // default cyan
|
'related-icon-offer' // default
|
||||||
],
|
],
|
||||||
'circle-stroke-width': 2,
|
'icon-size': 1,
|
||||||
'circle-stroke-color': '#ffffff'
|
'icon-allow-overlap': true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
@@ -403,7 +451,7 @@ const initClientClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
'text-field': ['get', 'name'],
|
'text-field': ['get', 'name'],
|
||||||
'text-size': 11,
|
'text-size': 11,
|
||||||
'text-anchor': 'top',
|
'text-anchor': 'top',
|
||||||
'text-offset': [0, 1.2]
|
'text-offset': [0, 1.5]
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'text-color': '#ffffff',
|
'text-color': '#ffffff',
|
||||||
@@ -413,19 +461,19 @@ const initClientClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Click handlers for related points
|
// Click handlers for related points
|
||||||
map.on('click', `${props.mapId}-related-circles`, (e) => {
|
map.on('click', `${props.mapId}-related-points`, (e) => {
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-circles`] })
|
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-points`] })
|
||||||
const feature = features[0]
|
const feature = features[0]
|
||||||
if (!feature) return
|
if (!feature) return
|
||||||
const props_data = feature.properties as Record<string, any> | undefined
|
const props_data = feature.properties as Record<string, any> | undefined
|
||||||
emit('select-item', props_data?.uuid, props_data)
|
emit('select-item', props_data?.uuid, props_data)
|
||||||
})
|
})
|
||||||
|
|
||||||
map.on('mouseenter', `${props.mapId}-related-circles`, () => {
|
map.on('mouseenter', `${props.mapId}-related-points`, () => {
|
||||||
map.getCanvas().style.cursor = 'pointer'
|
map.getCanvas().style.cursor = 'pointer'
|
||||||
})
|
})
|
||||||
|
|
||||||
map.on('mouseleave', `${props.mapId}-related-circles`, () => {
|
map.on('mouseleave', `${props.mapId}-related-points`, () => {
|
||||||
map.getCanvas().style.cursor = ''
|
map.getCanvas().style.cursor = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -564,27 +612,28 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Related points layer (for Info mode - colored by type)
|
// Related points layer (for Info mode - icons by type)
|
||||||
|
await loadRelatedPointIcons(map)
|
||||||
|
|
||||||
map.addSource(relatedSourceId.value, {
|
map.addSource(relatedSourceId.value, {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: relatedPointsGeoJson.value
|
data: relatedPointsGeoJson.value
|
||||||
})
|
})
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: `${props.mapId}-related-circles`,
|
id: `${props.mapId}-related-points`,
|
||||||
type: 'circle',
|
type: 'symbol',
|
||||||
source: relatedSourceId.value,
|
source: relatedSourceId.value,
|
||||||
paint: {
|
layout: {
|
||||||
'circle-radius': 8,
|
'icon-image': [
|
||||||
'circle-color': [
|
|
||||||
'match',
|
'match',
|
||||||
['get', 'type'],
|
['get', 'type'],
|
||||||
'hub', '#22c55e', // green
|
'hub', 'related-icon-hub',
|
||||||
'supplier', '#3b82f6', // blue
|
'supplier', 'related-icon-supplier',
|
||||||
'offer', '#f97316', // orange
|
'offer', 'related-icon-offer',
|
||||||
'#06b6d4' // default cyan
|
'related-icon-offer' // default
|
||||||
],
|
],
|
||||||
'circle-stroke-width': 2,
|
'icon-size': 1,
|
||||||
'circle-stroke-color': '#ffffff'
|
'icon-allow-overlap': true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
@@ -595,7 +644,7 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
'text-field': ['get', 'name'],
|
'text-field': ['get', 'name'],
|
||||||
'text-size': 11,
|
'text-size': 11,
|
||||||
'text-anchor': 'top',
|
'text-anchor': 'top',
|
||||||
'text-offset': [0, 1.2]
|
'text-offset': [0, 1.5]
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'text-color': '#ffffff',
|
'text-color': '#ffffff',
|
||||||
@@ -605,19 +654,19 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Click handlers for related points
|
// Click handlers for related points
|
||||||
map.on('click', `${props.mapId}-related-circles`, (e) => {
|
map.on('click', `${props.mapId}-related-points`, (e) => {
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-circles`] })
|
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-points`] })
|
||||||
const feature = features[0]
|
const feature = features[0]
|
||||||
if (!feature) return
|
if (!feature) return
|
||||||
const props_data = feature.properties as Record<string, any> | undefined
|
const props_data = feature.properties as Record<string, any> | undefined
|
||||||
emit('select-item', props_data?.uuid, props_data)
|
emit('select-item', props_data?.uuid, props_data)
|
||||||
})
|
})
|
||||||
|
|
||||||
map.on('mouseenter', `${props.mapId}-related-circles`, () => {
|
map.on('mouseenter', `${props.mapId}-related-points`, () => {
|
||||||
map.getCanvas().style.cursor = 'pointer'
|
map.getCanvas().style.cursor = 'pointer'
|
||||||
})
|
})
|
||||||
|
|
||||||
map.on('mouseleave', `${props.mapId}-related-circles`, () => {
|
map.on('mouseleave', `${props.mapId}-related-points`, () => {
|
||||||
map.getCanvas().style.cursor = ''
|
map.getCanvas().style.cursor = ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,13 @@ export function useCatalogHubs() {
|
|||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
offset,
|
offset,
|
||||||
transportType,
|
transportType,
|
||||||
country
|
country,
|
||||||
|
...(filterBounds.value && {
|
||||||
|
west: filterBounds.value.west,
|
||||||
|
south: filterBounds.value.south,
|
||||||
|
east: filterBounds.value.east,
|
||||||
|
north: filterBounds.value.north
|
||||||
|
})
|
||||||
},
|
},
|
||||||
'public',
|
'public',
|
||||||
'geo'
|
'geo'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const isInitialized = ref(false)
|
|||||||
// Filter state
|
// Filter state
|
||||||
const filterSupplierUuid = ref<string | null>(null)
|
const filterSupplierUuid = ref<string | null>(null)
|
||||||
const filterHubUuid = ref<string | null>(null)
|
const filterHubUuid = ref<string | null>(null)
|
||||||
|
const filterBounds = ref<{ west: number; south: number; east: number; north: number } | null>(null)
|
||||||
|
|
||||||
export function useCatalogProducts() {
|
export function useCatalogProducts() {
|
||||||
const { execute } = useGraphQL()
|
const { execute } = useGraphQL()
|
||||||
@@ -117,7 +118,15 @@ export function useCatalogProducts() {
|
|||||||
// All products from graph
|
// All products from graph
|
||||||
data = await execute(
|
data = await execute(
|
||||||
ProductsListDocument,
|
ProductsListDocument,
|
||||||
{ limit: 500 },
|
{
|
||||||
|
limit: 500,
|
||||||
|
...(filterBounds.value && {
|
||||||
|
west: filterBounds.value.west,
|
||||||
|
south: filterBounds.value.south,
|
||||||
|
east: filterBounds.value.east,
|
||||||
|
north: filterBounds.value.north
|
||||||
|
})
|
||||||
|
},
|
||||||
'public',
|
'public',
|
||||||
'geo'
|
'geo'
|
||||||
)
|
)
|
||||||
@@ -168,11 +177,12 @@ export function useCatalogProducts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Products don't have coordinates directly (they're an aggregation of offers)
|
// Products are filtered by offer locations within bounds
|
||||||
// Bounds filtering would require a new backend query that filters by offer locations
|
const setBoundsFilter = (bounds: { west: number; south: number; east: number; north: number } | null) => {
|
||||||
// For now, this is a no-op - products show all regardless of map bounds
|
filterBounds.value = bounds
|
||||||
const setBoundsFilter = (_bounds: { west: number; south: number; east: number; north: number } | null) => {
|
if (isInitialized.value) {
|
||||||
// No-op: products are not filterable by map bounds in current implementation
|
fetchProducts()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -47,7 +47,16 @@ export function useCatalogSuppliers() {
|
|||||||
// Default: fetch all suppliers from GEO (graph-based)
|
// Default: fetch all suppliers from GEO (graph-based)
|
||||||
const data = await execute(
|
const data = await execute(
|
||||||
SuppliersListDocument,
|
SuppliersListDocument,
|
||||||
{ limit: PAGE_SIZE, offset },
|
{
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset,
|
||||||
|
...(filterBounds.value && {
|
||||||
|
west: filterBounds.value.west,
|
||||||
|
south: filterBounds.value.south,
|
||||||
|
east: filterBounds.value.east,
|
||||||
|
north: filterBounds.value.north
|
||||||
|
})
|
||||||
|
},
|
||||||
'public',
|
'public',
|
||||||
'geo'
|
'geo'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query HubsList($limit: Int, $offset: Int, $country: String, $transportType: String) {
|
query HubsList($limit: Int, $offset: Int, $country: String, $transportType: String, $west: Float, $south: Float, $east: Float, $north: Float) {
|
||||||
hubsList(limit: $limit, offset: $offset, country: $country, transportType: $transportType) {
|
hubsList(limit: $limit, offset: $offset, country: $country, transportType: $transportType, west: $west, south: $south, east: $east, north: $north) {
|
||||||
uuid
|
uuid
|
||||||
name
|
name
|
||||||
latitude
|
latitude
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query ProductsList($limit: Int, $offset: Int) {
|
query ProductsList($limit: Int, $offset: Int, $west: Float, $south: Float, $east: Float, $north: Float) {
|
||||||
productsList(limit: $limit, offset: $offset) {
|
productsList(limit: $limit, offset: $offset, west: $west, south: $south, east: $east, north: $north) {
|
||||||
uuid
|
uuid
|
||||||
name
|
name
|
||||||
offersCount
|
offersCount
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query SuppliersList($limit: Int, $offset: Int, $country: String) {
|
query SuppliersList($limit: Int, $offset: Int, $country: String, $west: Float, $south: Float, $east: Float, $north: Float) {
|
||||||
suppliersList(limit: $limit, offset: $offset, country: $country) {
|
suppliersList(limit: $limit, offset: $offset, country: $country, west: $west, south: $south, east: $east, north: $north) {
|
||||||
uuid
|
uuid
|
||||||
name
|
name
|
||||||
latitude
|
latitude
|
||||||
|
|||||||
Reference in New Issue
Block a user