preview map editor 60%
This commit is contained in:
@@ -4,6 +4,7 @@ import { type CSSProperties } from "react";
|
||||
import { Entity } from "@/api/entities";
|
||||
import { Feature } from "@/lib/useEditorState";
|
||||
import {
|
||||
EntityGeometryPreset,
|
||||
EntityTypeGroupId,
|
||||
EntityTypeOption,
|
||||
findEntityTypeOption,
|
||||
@@ -74,20 +75,22 @@ export default function SelectedGeometryPanel({
|
||||
entityFormStatus,
|
||||
}: Props) {
|
||||
const groupedEntityTypeOptions = groupEntityTypeOptions(entityTypeOptions);
|
||||
const allowedGroupIds = selectedFeature
|
||||
? getAllowedGroupIdsForGeometry(selectedFeature.geometry.type)
|
||||
const featureGeometryPreset = selectedFeature
|
||||
? resolveFeatureGeometryPreset(selectedFeature)
|
||||
: null;
|
||||
const allowedGroupIds = featureGeometryPreset
|
||||
? getAllowedGroupIdsForPreset(featureGeometryPreset)
|
||||
: [];
|
||||
const visibleGroupedEntityTypeOptions = groupedEntityTypeOptions.filter((group) =>
|
||||
allowedGroupIds.includes(group.id)
|
||||
);
|
||||
const selectedTypeOption = findEntityTypeOption(entityForm.type_id);
|
||||
const hasCurrentTypeOption = entityTypeOptions.some((option) => option.value === entityForm.type_id);
|
||||
const geometryBucket = selectedFeature ? toGeometryBucket(selectedFeature.geometry.type) : null;
|
||||
const selectedTypeBucket = selectedTypeOption ? toTypeBucket(selectedTypeOption) : null;
|
||||
const isGeometryCompatible =
|
||||
geometryBucket && selectedTypeBucket
|
||||
? geometryBucket === selectedTypeBucket
|
||||
: true;
|
||||
const hasCurrentVisibleTypeOption = visibleGroupedEntityTypeOptions.some((group) =>
|
||||
group.options.some((option) => option.value === entityForm.type_id)
|
||||
);
|
||||
const isGeometryCompatible = selectedTypeOption
|
||||
? allowedGroupIds.includes(selectedTypeOption.groupId)
|
||||
: true;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -117,6 +120,9 @@ export default function SelectedGeometryPanel({
|
||||
<div style={{ color: "#cbd5e1" }}>
|
||||
Binding hiện tại: {selectedFeatureBindingSummary}
|
||||
</div>
|
||||
<div style={{ color: "#cbd5e1" }}>
|
||||
Geometry preset: {formatGeometryPresetLabel(featureGeometryPreset)}
|
||||
</div>
|
||||
|
||||
<div style={{ color: "#94a3b8", fontSize: "12px" }}>
|
||||
Entities đã chọn:
|
||||
@@ -166,60 +172,10 @@ export default function SelectedGeometryPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
value={entitySearchQuery}
|
||||
onChange={(event) => onEntitySearchQueryChange(event.target.value)}
|
||||
placeholder="Search entity theo name..."
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<select
|
||||
value={selectedSearchEntityId || ""}
|
||||
onChange={(event) =>
|
||||
onSelectSearchEntityId(event.target.value ? event.target.value : null)
|
||||
}
|
||||
disabled={isEntitySubmitting || isEntitySearchLoading}
|
||||
style={entityInputStyle}
|
||||
>
|
||||
<option value="">-- Chọn entity từ kết quả search --</option>
|
||||
{entitySearchResults.map((entity) => (
|
||||
<option key={entity.id} value={entity.id}>
|
||||
{entity.name} ({entity.id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddSelectedSearchEntity}
|
||||
disabled={isEntitySubmitting || isEntitySearchLoading}
|
||||
style={secondaryActionButtonStyle}
|
||||
>
|
||||
Chọn để thêm
|
||||
</button>
|
||||
{isEntitySearchLoading ? (
|
||||
<div style={{ color: "#93c5fd", fontSize: "12px" }}>
|
||||
Đang tìm entity...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ color: "#94a3b8", fontSize: "12px" }}>
|
||||
Geometry phải có ít nhất 1 entity để Save.
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={entityForm.name}
|
||||
onChange={(event) => onEntityFormChange("name", event.target.value)}
|
||||
placeholder="Tên entity mới"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={entityForm.slug}
|
||||
onChange={(event) => onEntityFormChange("slug", event.target.value)}
|
||||
placeholder="Slug"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
@@ -230,80 +186,146 @@ export default function SelectedGeometryPanel({
|
||||
background: "#0f172a",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#e2e8f0", fontWeight: 600, fontSize: "12px" }}>
|
||||
Entity type presets
|
||||
<div style={{ color: "#e2e8f0", fontWeight: 700, fontSize: "12px" }}>
|
||||
Metadata geometry (áp dụng cho cả 2 luồng bên dưới)
|
||||
</div>
|
||||
<div style={{ color: "#94a3b8", fontSize: "11px" }}>
|
||||
Chọn nhanh theo nhóm hình học bạn cần vẽ.
|
||||
`time_start`, `time_end`, `binding` sẽ được dùng cho cả luồng gắn entity có sẵn
|
||||
và luồng tạo entity mới.
|
||||
</div>
|
||||
<input
|
||||
value={geometryMetaForm.time_start}
|
||||
onChange={(event) => onGeometryMetaFormChange("time_start", event.target.value)}
|
||||
placeholder="time_start"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={geometryMetaForm.time_end}
|
||||
onChange={(event) => onGeometryMetaFormChange("time_end", event.target.value)}
|
||||
placeholder="time_end"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={geometryMetaForm.binding}
|
||||
onChange={(event) => onGeometryMetaFormChange("binding", event.target.value)}
|
||||
placeholder="binding ids (vd: geo-id-1, geo-id-2)"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "8px",
|
||||
border: "1px solid #1f3b5a",
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
background: "#0f172a",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#bfdbfe", fontWeight: 700, fontSize: "12px" }}>
|
||||
Luồng 1: Gắn entity có sẵn
|
||||
</div>
|
||||
<div style={{ color: "#93c5fd", fontSize: "11px" }}>
|
||||
Dùng khi entity đã tồn tại. Tìm kiếm, thêm vào danh sách rồi bấm nút áp dụng.
|
||||
</div>
|
||||
<input
|
||||
value={entitySearchQuery}
|
||||
onChange={(event) => onEntitySearchQueryChange(event.target.value)}
|
||||
placeholder="Search entity theo name..."
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<select
|
||||
value={selectedSearchEntityId || ""}
|
||||
onChange={(event) =>
|
||||
onSelectSearchEntityId(event.target.value ? event.target.value : null)
|
||||
}
|
||||
disabled={isEntitySubmitting || isEntitySearchLoading}
|
||||
style={entityInputStyle}
|
||||
>
|
||||
<option value="">-- Chọn entity từ kết quả search --</option>
|
||||
{entitySearchResults.map((entity) => (
|
||||
<option key={entity.id} value={entity.id}>
|
||||
{entity.name} ({entity.id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddSelectedSearchEntity}
|
||||
disabled={isEntitySubmitting || isEntitySearchLoading}
|
||||
style={secondaryActionButtonStyle}
|
||||
>
|
||||
Thêm entity đã chọn vào danh sách gắn
|
||||
</button>
|
||||
{isEntitySearchLoading ? (
|
||||
<div style={{ color: "#93c5fd", fontSize: "12px" }}>
|
||||
Đang tìm entity...
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onApplyEntitiesForSelectedGeometry}
|
||||
disabled={isEntitySubmitting}
|
||||
style={{
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "7px 8px",
|
||||
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
|
||||
background: "#0f766e",
|
||||
color: "#ffffff",
|
||||
opacity: isEntitySubmitting ? 0.7 : 1,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Áp dụng danh sách entity + metadata
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "8px",
|
||||
border: "1px solid #1e3a8a",
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
background: "#0f172a",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#bfdbfe", fontWeight: 700, fontSize: "12px" }}>
|
||||
Luồng 2: Tạo entity mới rồi gắn
|
||||
</div>
|
||||
<div style={{ color: "#93c5fd", fontSize: "11px" }}>
|
||||
Dùng khi chưa có entity phù hợp. Điền form bên dưới rồi bấm nút tạo + gắn.
|
||||
</div>
|
||||
|
||||
{visibleGroupedEntityTypeOptions.map((group) => {
|
||||
const color = GROUP_COLORS[group.id];
|
||||
return (
|
||||
<div
|
||||
key={group.id}
|
||||
style={{
|
||||
border: `1px solid ${color.border}`,
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
background: color.background,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: color.text, fontWeight: 600, fontSize: "12px" }}>
|
||||
{group.label}
|
||||
</span>
|
||||
<span style={{ color: color.mutedText, fontSize: "11px" }}>
|
||||
{group.geometryLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px" }}>
|
||||
{group.options.map((option) => {
|
||||
const active = option.value === entityForm.type_id;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => onEntityFormChange("type_id", option.value)}
|
||||
disabled={isEntitySubmitting}
|
||||
style={{
|
||||
border: active
|
||||
? `1px solid ${color.activeBorder}`
|
||||
: `1px solid ${color.chipBorder}`,
|
||||
borderRadius: "999px",
|
||||
padding: "4px 9px",
|
||||
background: active ? color.activeBackground : color.chipBackground,
|
||||
color: active ? color.activeText : color.text,
|
||||
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
|
||||
fontSize: "11px",
|
||||
fontWeight: active ? 600 : 500,
|
||||
opacity: isEntitySubmitting ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<input
|
||||
value={entityForm.name}
|
||||
onChange={(event) => onEntityFormChange("name", event.target.value)}
|
||||
placeholder="Tên entity mới"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={entityForm.slug}
|
||||
onChange={(event) => onEntityFormChange("slug", event.target.value)}
|
||||
placeholder="Slug"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<div style={{ color: "#e2e8f0", fontWeight: 600, fontSize: "12px" }}>
|
||||
Chọn loại entity
|
||||
</div>
|
||||
<select
|
||||
value={entityForm.type_id}
|
||||
onChange={(event) => onEntityFormChange("type_id", event.target.value)}
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
>
|
||||
{!hasCurrentTypeOption && entityForm.type_id ? (
|
||||
{!hasCurrentVisibleTypeOption && entityForm.type_id ? (
|
||||
<option value={entityForm.type_id}>
|
||||
Custom Type ({entityForm.type_id})
|
||||
</option>
|
||||
@@ -334,32 +356,10 @@ export default function SelectedGeometryPanel({
|
||||
|
||||
{!isGeometryCompatible && selectedTypeOption ? (
|
||||
<div style={{ color: "#fbbf24", fontSize: "12px" }}>
|
||||
Type <b>{selectedTypeOption.label}</b> hợp với {selectedTypeBucket}, nhưng geometry hiện tại là {geometryBucket}.
|
||||
Type <b>{selectedTypeOption.label}</b> thuộc nhóm <b>{selectedTypeOption.groupLabel}</b>, nhưng geometry hiện tại là <b>{formatGeometryPresetLabel(featureGeometryPreset)}</b>.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
value={geometryMetaForm.time_start}
|
||||
onChange={(event) => onGeometryMetaFormChange("time_start", event.target.value)}
|
||||
placeholder="time_start"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={geometryMetaForm.time_end}
|
||||
onChange={(event) => onGeometryMetaFormChange("time_end", event.target.value)}
|
||||
placeholder="time_end"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
<input
|
||||
value={geometryMetaForm.binding}
|
||||
onChange={(event) => onGeometryMetaFormChange("binding", event.target.value)}
|
||||
placeholder="binding ids (vd: geo-id-1, geo-id-2)"
|
||||
disabled={isEntitySubmitting}
|
||||
style={entityInputStyle}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={onCreateEntityAndAttach}
|
||||
disabled={isEntitySubmitting}
|
||||
@@ -371,26 +371,12 @@ export default function SelectedGeometryPanel({
|
||||
background: "#2563eb",
|
||||
color: "#ffffff",
|
||||
opacity: isEntitySubmitting ? 0.7 : 1,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Tạo Entity + Gắn
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onApplyEntitiesForSelectedGeometry}
|
||||
disabled={isEntitySubmitting}
|
||||
style={{
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
padding: "7px 8px",
|
||||
cursor: isEntitySubmitting ? "not-allowed" : "pointer",
|
||||
background: "#0f766e",
|
||||
color: "#ffffff",
|
||||
opacity: isEntitySubmitting ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
Áp dụng Entities + Metadata
|
||||
Tạo entity mới + gắn + áp metadata
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{changeCount > 0 ? (
|
||||
<div style={{ color: "#fca5a5", fontSize: "12px" }}>
|
||||
@@ -438,80 +424,42 @@ const secondaryActionButtonStyle: CSSProperties = {
|
||||
color: "#ffffff",
|
||||
};
|
||||
|
||||
const GROUP_COLORS: Record<
|
||||
EntityTypeGroupId,
|
||||
{
|
||||
background: string;
|
||||
border: string;
|
||||
text: string;
|
||||
mutedText: string;
|
||||
chipBackground: string;
|
||||
chipBorder: string;
|
||||
activeBackground: string;
|
||||
activeBorder: string;
|
||||
activeText: string;
|
||||
function resolveFeatureGeometryPreset(feature: Feature): EntityGeometryPreset {
|
||||
const explicitPreset = normalizeGeometryPreset(feature.properties.geometry_preset);
|
||||
if (explicitPreset) return explicitPreset;
|
||||
|
||||
const semanticType = normalizeTypeId(feature.properties.type) || normalizeTypeId(feature.properties.entity_type_id);
|
||||
if (semanticType) {
|
||||
const option = findEntityTypeOption(semanticType);
|
||||
if (option) return option.geometryPreset;
|
||||
}
|
||||
> = {
|
||||
split: {
|
||||
background: "#3f1d24",
|
||||
border: "#7f1d1d",
|
||||
text: "#fecaca",
|
||||
mutedText: "#fda4af",
|
||||
chipBackground: "#4c1d27",
|
||||
chipBorder: "#7f1d1d",
|
||||
activeBackground: "#7f1d1d",
|
||||
activeBorder: "#fecaca",
|
||||
activeText: "#ffffff",
|
||||
},
|
||||
route: {
|
||||
background: "#082f49",
|
||||
border: "#0c4a6e",
|
||||
text: "#bae6fd",
|
||||
mutedText: "#7dd3fc",
|
||||
chipBackground: "#0c4a6e",
|
||||
chipBorder: "#155e75",
|
||||
activeBackground: "#0369a1",
|
||||
activeBorder: "#bae6fd",
|
||||
activeText: "#f0f9ff",
|
||||
},
|
||||
area_polygon: {
|
||||
background: "#1f3a2a",
|
||||
border: "#166534",
|
||||
text: "#bbf7d0",
|
||||
mutedText: "#86efac",
|
||||
chipBackground: "#14532d",
|
||||
chipBorder: "#166534",
|
||||
activeBackground: "#15803d",
|
||||
activeBorder: "#bbf7d0",
|
||||
activeText: "#f0fdf4",
|
||||
},
|
||||
area_circle: {
|
||||
background: "#3f2b05",
|
||||
border: "#92400e",
|
||||
text: "#fde68a",
|
||||
mutedText: "#fcd34d",
|
||||
chipBackground: "#78350f",
|
||||
chipBorder: "#92400e",
|
||||
activeBackground: "#b45309",
|
||||
activeBorder: "#fde68a",
|
||||
activeText: "#fffbeb",
|
||||
},
|
||||
point: {
|
||||
background: "#2e1065",
|
||||
border: "#6d28d9",
|
||||
text: "#ddd6fe",
|
||||
mutedText: "#c4b5fd",
|
||||
chipBackground: "#4c1d95",
|
||||
chipBorder: "#6d28d9",
|
||||
activeBackground: "#7c3aed",
|
||||
activeBorder: "#ddd6fe",
|
||||
activeText: "#f5f3ff",
|
||||
},
|
||||
};
|
||||
|
||||
type GeometryBucket = "point" | "line" | "polygon";
|
||||
return mapGeometryTypeToPreset(feature.geometry.type);
|
||||
}
|
||||
|
||||
function toGeometryBucket(geometryType: Feature["geometry"]["type"]): GeometryBucket {
|
||||
function normalizeGeometryPreset(value: unknown): EntityGeometryPreset | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (
|
||||
normalized === "point" ||
|
||||
normalized === "line" ||
|
||||
normalized === "polygon" ||
|
||||
normalized === "circle-area"
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTypeId(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.length ? normalized : null;
|
||||
}
|
||||
|
||||
function mapGeometryTypeToPreset(
|
||||
geometryType: Feature["geometry"]["type"]
|
||||
): EntityGeometryPreset {
|
||||
if (geometryType === "Point" || geometryType === "MultiPoint") {
|
||||
return "point";
|
||||
}
|
||||
@@ -521,26 +469,28 @@ function toGeometryBucket(geometryType: Feature["geometry"]["type"]): GeometryBu
|
||||
return "polygon";
|
||||
}
|
||||
|
||||
function toTypeBucket(option: EntityTypeOption): GeometryBucket {
|
||||
if (option.geometryPreset === "point") {
|
||||
return "point";
|
||||
}
|
||||
if (option.geometryPreset === "line") {
|
||||
return "line";
|
||||
}
|
||||
return "polygon";
|
||||
}
|
||||
|
||||
function getAllowedGroupIdsForGeometry(
|
||||
geometryType: Feature["geometry"]["type"]
|
||||
function getAllowedGroupIdsForPreset(
|
||||
geometryPreset: EntityGeometryPreset
|
||||
): EntityTypeGroupId[] {
|
||||
if (geometryType === "Point" || geometryType === "MultiPoint") {
|
||||
if (geometryPreset === "point") {
|
||||
return ["point"];
|
||||
}
|
||||
|
||||
if (geometryType === "LineString" || geometryType === "MultiLineString") {
|
||||
return ["split", "route"];
|
||||
if (geometryPreset === "line") {
|
||||
return ["line"];
|
||||
}
|
||||
|
||||
return ["area_polygon", "area_circle"];
|
||||
if (geometryPreset === "circle-area") {
|
||||
return ["circle"];
|
||||
}
|
||||
|
||||
return ["polygon"];
|
||||
}
|
||||
|
||||
function formatGeometryPresetLabel(preset: EntityGeometryPreset | null): string {
|
||||
if (preset === "point") return "point - Điểm";
|
||||
if (preset === "line") return "line - Tuyến";
|
||||
if (preset === "circle-area") return "circle - Tròn";
|
||||
if (preset === "polygon") return "polygon - Đa giác";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user