pcl_openmap_OpenMap教程5 – 3层GIS应用程序

pcl_openmap

1.简介

欢迎使用OpenMap系列教程的第5个教程。 OpenMap是一个免费的开源Java GIS库。

这是以前的教程列表:

  • 在第一个教程中,我们创建了一个基本的OpenMap GIS应用程序,该应用程序在JFrame中显示一个从文件系统加载的具有一个形状图层的地图。 该教程基于com.bbn.openmap.app.example.SimpleMap
  • 在第二个教程中,我们扩展了基本应用程序以使用MapHandler
  • 在第三个教程中,我们看到了如何利用BeanContext技术在openmap.properties文件中声明我们的类并以声明方式构建整个应用程序。
  • 第四个教程介绍了地图图层。

在本教程中,我们将讨论如何基于OpenMap构建3层GIS应用程序。 我们将在探索新的OpenMap功能方面稍作休息,并将主要回顾我们在先前教程中学到的内容。

2.需求和架构概述

您的老板或客户提出了一些要求。 在第一个冲刺(例如Scrum)中,应用程序应该能够:

  • 从数据库读取数据/向数据库写入数据
  • 在GIS地图上显示数据
  • 与数据交互并显示其属性
  • 将地理数据移动到其他位置并将其保存回数据库
  • 创建/更新/删除地图数据

您可能会说得很简单,然后继续草绘应用程序的3层架构草案:

OpenMap5-图1

图1: 三层架构

3层架构遵循“ 模型-视图-控制器(MVC)”架构模式。 从数据库(后端)创建模型 。 我们的View是我们在之前的教程中构建的OpenMap GIS应用程序,能够将数据显示为点,线,多边形等。并且Controller将所有内容连接起来。
类似的架构是Model-View-ViewModel(MVVM) ,我们还将对其进行简要讨论。

3.技术

3.1后端

后端主要是数据库,或更准确地说,是数据库管理系统(DBMS) 。 在这里,您可以选择:

*具有或不具有地理空间扩展的关系数据库 (Oracle,MySQL,Postgresql,MS SQL Server,Sqlite,Hsqldb,JavaDB等)。 地理空间扩展存在于MySQL , Postgresql , Oracle , SQLite ; MS SQL Server 2008带有内置的空间扩展。
* 基于对象的空间数据库
*具有空间支持的No-SQL数据库(例如CassandraDB, CouchDB , MongoDB , Neo4j等)

优化了空间数据库或地理数据库,以存储和查询表示在几何空间中定义的对象的数据。 大多数空间数据库都允许根据OpenGIS规范表示简单的几何对象,例如点,线和面以及空间索引。 但是,您无需具有GeoSpatial数据库即可构建GIS应用程序,但是使用它会有好处。

3.2模型

您如何访问数据库以检索要用于Java应用程序的数据? 以下是您可以使用的可能技术的列表:

  • SQL查询数据库,即Java Database Connectivity或JDBC 。 这是传统方式(但是我们在2016年!)。 您需要“讲” SQL来查询数据库并在ResultSets检索数据,当您的应用程序遵循面向对象模型时(除非您的数据库也是面向对象或对象关系的)也不太方便。
  • 对象关系映射,例如Java Persistence API(JPA) 。 这是将数据库表映射到Java对象的现代方法。 NetBeans为您提供了一个不错的JPA映射向导。
  • 功能映射。 如果您是Java 8专家,并且喜欢lambda,那么为什么不使用λ表达式和Stream API而不是SQL查询或JPA? Speedment是一个Java库,使这个梦想成为现实。 这是SQL和Stream API之间的比较,以便查询数据。

3.3控制器

最后一个问题是如何将视图连接到模型? 这里的关键问题是各个组件之间的松耦合。 松散耦合使您可以使用另一种技术替换应用程序的任何层,而又不影响其他层(或进行有限的更改)。 有许多解决方案,例如:

  • Java 6 ServiceLoader
  • NetBeans查找API
  • Dukescript(MVVM) 。 将DukeScript用于客户端-服务器应用程序的好处之一是代码重用。 您可以在客户端和服务器上使用相同的模型类。 这是映射JPA和Dukescript的教程 。

4.构建我们的应用程序

我不会在这里探索所有这些技术。 请随意查看本文结尾处的参考。

在本文中,我们将看到如何使用模型的JPA和控制器的NetBeans Lookup API来构建MVC GIS应用程序。 在以后的文章中,我们将看到替代技术,例如用Speedment替换JPA和用Dukescript用MVVM替换MVC。

4.1我们的观点

在之前的文章中,我们已经创建了一个OpenMap应用程序。 让我们回顾一下并重构它。

我们的OpenMap应用程序包含以下文件层次结构:

  • openmap
    • DMSCoordInfoFormatter
    • DemoLayer
    • MyDrawingTool
    • OpenMap
  • openmap.properties

让我们这样重构它:

  • openmap
    • OpenMap.java
    • openmap.controller
    • openmap.model
    • openmap.view
      • DMSCoordInfoFormatter.java
      • DemoLayer.java
      • MyDrawingTool.java
  • openmap.properties

也不要忘记更新openmap.properties的路径。 上面的包结构描述了Model-View-Controller(MVC)设计模式。

在NetBeans中(而且在其他IDE),你可以很容易地应用重构(如移动一个文件或文件夹到另一个文件夹或重命名文件/文件夹)上的文件/文件夹,右键单击并选择子菜单重构下一个重构。

添加一个城市图层(来自OpenMap的原始openmap.properties ):

清单1 – openmap.properties –城市层

# These layers are turned on when the map is first started.  Order
# does not matter here...
openmap.startUpLayers=demo cities graticule shapePolitical# Layers listed here appear on the Map in the order of their names.
openmap.layers=demo cities graticule shapePolitical
...
###
# LocationLayer that holds cities.  The palette for this layer lets
# you turn on the names and declutter matrix, if you want.  The
# declutter matrix can get expensive at small scales.
cities.class=com.bbn.openmap.layer.location.LocationLayer
cities.prettyName=World Cities
cities.locationHandlers=csvcities
cities.useDeclutter=false
cities.declutterMatrix=com.bbn.openmap.layer.DeclutterMatrixcsvcities.class=com.bbn.openmap.layer.location.csv.CSVLocationHandler
csvcities.prettyName=World Cities
csvcities.locationFile=resources/map/cities.csv
csvcities.csvFileHasHeader=true
csvcities.locationColor=FF0000
csvcities.nameColor=008C54
csvcities.showNames=false
csvcities.showLocations=true
csvcities.nameIndex=0
csvcities.latIndex=5
csvcities.lonIndex=4
csvcities.csvFileHasHeader=true

并且不要忘记将cities.csv复制到resources/map

再次运行该应用程序以查看新层。

4.2我们的数据库架构

我们的数据库架构显示在下面的清单中。 它主要由一个Supplier表组成。 我们想在地图上将我们的供应商显示为GeoPoint

以下是在NetBeans中创建SQLite数据库的步骤(您可以选择任何喜欢的DBMS):

  1. 右键单击Libraries
  2. 从弹出菜单中选择添加JAR /文件夹...
  3. 导航到您从中下载SQLite的文件夹,然后选择sqlite-jdbc-xxx.jar
  4. 选择复制到库文件夹 ,然后单击打开 。 驱动程序应显示在“ 库”下。
  5. 单击窗口→服务菜单以显示服务选项卡。
  6. 展开数据库节点
  7. 右键单击“ 驱动程序”节点,然后选择“ 新驱动程序”
  8. 点击添加
  9. 导航到从SQLite网站下载sqlite-jdbc-xxxx.jar文件的位置 ; 驱动程序类应为org.sqlite.JDBCNameSQLite
  10. 单击确定SQLite应该列在Drivers
  11. 右键单击数据库,然后选择New Connection…
  12. 选择SQLite驱动程序,然后单击下一步
  13. 提供一个JDBC URL,例如jdbc:sqlite:C:\db\suppliers.sqlite-3并单击Finish 。 您的连接应显示在“ 数据库”下。
  14. 右键单击它,然后选择Connect…
  15. 右键单击表,然后选择Execute Command…
  16. 输入以下SQL语句,然后单击“ Run SQL按钮:

清单2 –供应商表

CREATE TABLE supplier (
SID             INTEGER           PRIMARY KEY,
NAME               VARCHAR2 (30)     NOT NULL,
CITY               VARCHAR2 (30)     NOT NULL,
TYPE            VARCHAR2 (10)     NOT NULL
CONSTRAINT TYPE CHECK (TYPE IN ('GROSS','RETAIL')),
LATITUDE        NUMBER (12,10)    NOT NULL
CONSTRAINT LATITUDE CHECK (LATITUDE BETWEEN -90.0000000000 AND 90.0000000000),
LONGITUDE           NUMBER (13,10)    NOT NULL
CONSTRAINT LONGITUDE CHECK (LONGITUDE BETWEEN -180.0000000000 AND 180.0000000000),
CONSTRAINT UID UNIQUE (SID, NAME, LATITUDE, LONGITUDE)
)

验证新表是否已创建并列在“ 表”下。 您可以将相同的样本数据添加到表中:

清单3 –样本数据

INSERT INTO supplier (NAME, CITY, TYPE, LATITUDE, LONGITUDE)
VALUES ('HP', 'ATHENS', 'GROSS', 38.1216011, 23.65486336);
INSERT INTO supplier (NAME, CITY, TYPE, LATITUDE, LONGITUDE)
VALUES ('DELL', 'BRUSSELS', 'RETAIL', 50.83704758, 4.367612362);
INSERT INTO supplier (NAME, CITY, TYPE, LATITUDE, LONGITUDE)
VALUES ('APPLE', 'LONDON', 'RETAIL', 51.48791122, -0.177998126);
INSERT INTO supplier (NAME, CITY, TYPE, LATITUDE, LONGITUDE)
VALUES ('TOSHIBA', 'PARIS', 'GROSS', 48.88155365, 2.432832718);

在继续操作之前,请不要忘记断开与数据库的连接。 由于SQLite是独立数据库,因此它主要是文件系统中的文件。 一次只能有一个应用程序可以访问它。 如果从“ 服务”选项卡连接到它,并尝试同时从OpenMap应用程序访问它,则会出现数据库被锁定的异常。 对于诸如PostgresqlMS SQL Server之类的“实际” DBMS,情况并非如此,它们可以并发访问。

4.3建立模型

让我们根据以上模式构建一个JPA模型。 NetBeans提供了很好的JPA支持:

  1. 右键单击openmap.model
  2. New → Other → Persistence → Entity Classes from Database选择New → Other → Persistence → Entity Classes from Database然后单击下一步。
  3. 选择您的数据库连接suppliers.sqlite-3
  4. 从“ 可用表”中选择suppliers ,然后单击“ 添加”以将其移动到“ 选定表”中
  5. 点击下一步
  6. 在第3步中,仅选中“ 为持久字段生成命名查询注释” ,然后单击“ 下一步”
  7. 在第4步中,取消选中所有复选框,然后单击Finish

向导在openmap.model下创建了一个新类Suppliers和一个不必要的SupplierPK 。 它还创建了文件META-INF/persistence.xml ,其中包含有关数据库的连接信息:

清单4 – persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"><persistence-unit name="OpenMapPU" transaction-type="RESOURCE_LOCAL"><provider>org.eclipse.persistence.jpa.PersistenceProvider</provider><class>openmap.model.Supplier</class><properties><property name="javax.persistence.jdbc.url" value="jdbc:sqlite:C:\db\suppliers.sqlite3"/><property name="javax.persistence.jdbc.user" value=""/><property name="javax.persistence.jdbc.driver" value="org.sqlite.JDBC"/><property name="javax.persistence.jdbc.password" value=""/></properties></persistence-unit>
</persistence>

由于包含主键定义的架构(请参见清单2),向导将为主键生成SupplierPK类。 这不是必需的,因此删除此类并从Supplier类中删除此字段及其引用。 修改您的Supplier类,使其类似于以下清单:

清单5 – Supplier.java

package openmap.model;import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Transient;/**** @author ikost*/
@Entity
@Table(name = "supplier")
@NamedQueries({@NamedQuery(name = "Supplier.findAll", query = "SELECT s FROM Supplier s"),@NamedQuery(name = "Supplier.findBySid", query = "SELECT s FROM Supplier s WHERE s.sid = :sid"),@NamedQuery(name = "Supplier.findByName", query = "SELECT s FROM Supplier s WHERE s.name = :name"),@NamedQuery(name = "Supplier.findByCity", query = "SELECT s FROM Supplier s WHERE s.city = :city"),@NamedQuery(name = "Supplier.findByType", query = "SELECT s FROM Supplier s WHERE s.type = :type"),@NamedQuery(name = "Supplier.findByLatitude", query = "SELECT s FROM Supplier s WHERE s.latitude = :latitude"),@NamedQuery(name = "Supplier.findByLongitude", query = "SELECT s FROM Supplier s WHERE s.longitude = :longitude")})
public class Supplier implements Serializable {private static final long serialVersionUID = 1L;@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Basic(optional = false)@Column(name = "SID")private int sid;@Basic(optional = false)@Column(name = "NAME")private String name;@Basic(optional = false)@Column(name = "CITY")private String city;@Basic(optional = false)@Column(name = "TYPE")@Enumerated(EnumType.STRING)private String type;@Basic(optional = false)@Column(name = "LATITUDE")private double latitude;@Basic(optional = false)@Column(name = "LONGITUDE")private double longitude;public enum TYPE {GROSS, RETAIL};public Supplier() {}public Supplier(int id) {this.sid = id;}public Supplier(int id, String name, String city,TYPE type, double latitude, double longitude) {this.sid = id;this.name = name;this.city = city;this.type = type;this.latitude = latitude;this.longitude = longitude;}public int getSid() {return sid;}public void setSid(int sid) {this.sid = sid;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getCity() {return city;}public void setCity(String city) {this.city = city;}public TYPE getType() {return type;}public void setType(TYPE type) {this.type = type;}public double getLatitude() {return latitude;}public void setLatitude(double latitude) {this.latitude = latitude;}public double getLongitude() {return longitude;}public void setLongitude(double longitude) {this.longitude = longitude;}@Overridepublic int hashCode() {return sid;}@Overridepublic boolean equals(Object object) {if (!(object instanceof Supplier)) {return false;}Supplier other = (Supplier) object;if (this.sid != other.sid) {return false;}return true;}@Overridepublic String toString() {return "openmap.model.Supplier[ sid =" + sid + " ]";}}

JPA 2.1为枚举提供了映射支持(请参见上面清单中的type字段)。

4.4建立您的控制器

NetBeans还可轻松为模型生成控制器。

  1. 右键单击openmap.controller
  2. New → Other → Persistence → JPA Controller Classes from Entity Classes选择New → Other → Persistence → JPA Controller Classes from Entity Classes ,然后单击下一步
  3. 从“ 可用实体类 ”列表中选择“ Supplier ”,然后单击“ 添加”将其移动到“ 选定实体类 ”列表中。
  4. 点击下一步
  5. 在第3步中,将包固定为openmap.controller
  6. 点击完成

该向导创建了SupplierJpaController以及3个异常文件。 现在,视图可以访问此控制器以便对模型执行操作。

清单6 – SupplierJpaController.java

package openmap.controller;import java.io.Serializable;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Query;
import javax.persistence.EntityNotFoundException;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import openmap.controller.exceptions.NonexistentEntityException;
import openmap.model.Supplier;/**** @author ikost*/
public class SupplierJpaController implements Serializable {public SupplierJpaController(EntityManagerFactory emf) {this.emf = emf;}private EntityManagerFactory emf = null;public EntityManager getEntityManager() {return emf.createEntityManager();}public void create(Supplier supplier) {EntityManager em = null;try {em = getEntityManager();em.getTransaction().begin();em.persist(supplier);em.getTransaction().commit();} finally {if (em != null) {em.close();}}}public void edit(Supplier supplier) throws NonexistentEntityException, Exception {EntityManager em = null;try {em = getEntityManager();em.getTransaction().begin();supplier = em.merge(supplier);em.getTransaction().commit();} catch (Exception ex) {String msg = ex.getLocalizedMessage();if (msg == null || msg.length() == 0) {int id = supplier.getSid();if (findSupplier(id) == null) {throw new NonexistentEntityException("The supplier with id " + id + " no longer exists.");}}throw ex;} finally {if (em != null) {em.close();}}}public void destroy(int id) throws NonexistentEntityException {EntityManager em = null;try {em = getEntityManager();em.getTransaction().begin();Supplier supplier;try {supplier = em.getReference(Supplier.class, id);supplier.getSid();} catch (EntityNotFoundException enfe) {throw new NonexistentEntityException("The supplier with id " + id + " no longer exists.", enfe);}em.remove(supplier);em.getTransaction().commit();} finally {if (em != null) {em.close();}}}public List<Supplier> findSupplierEntities() {return findSupplierEntities(true, -1, -1);}public List<Supplier> findSupplierEntities(int maxResults, int firstResult) {return findSupplierEntities(false, maxResults, firstResult);}private List<Supplier> findSupplierEntities(boolean all, int maxResults, int firstResult) {EntityManager em = getEntityManager();try {CriteriaQuery cq = em.getCriteriaBuilder().createQuery();cq.select(cq.from(Supplier.class));Query q = em.createQuery(cq);if (!all) {q.setMaxResults(maxResults);q.setFirstResult(firstResult);}return q.getResultList();} finally {em.close();}}public Supplier findSupplier(int id) {EntityManager em = getEntityManager();try {return em.find(Supplier.class, id);} finally {em.close();}}public int getSupplierCount() {EntityManager em = getEntityManager();try {CriteriaQuery cq = em.getCriteriaBuilder().createQuery();Root<Supplier> rt = cq.from(Supplier.class);cq.select(em.getCriteriaBuilder().count(rt));Query q = em.createQuery(cq);return ((Long) q.getSingleResult()).intValue();} finally {em.close();}}
}

在此示例中,我们只有一个域对象Supplier但在实际应用中,您将有许多域对象。 一个常见的解决方案是创建一个Facade ,该Facade仅将视图所需的方法公开,而将其余的隐藏。 在下面的内容中,我们展示如何使用NetBeans Lookup API创建松散耦合的Facade IDBManager (请参阅参考资料)。

清单7 – IDBManager.java

package openmap.controller;import java.util.List;
import openmap.model.Supplier;/*** A facade of our controllers.** @author ikost*/
public interface IDBManager {List getSuppliers();
}

单击IDBManager左侧的blob,然后选择“ 实现接口” 。 如下表所示修改DBManager实现,以将其转换为服务提供者:

清单8 – DBManager.java

package openmap.controller;import java.util.List;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import openmap.model.Supplier;
import org.openide.util.lookup.ServiceProvider;@ServiceProvider(service = IDBManager.class)
public class DBManager implements IDBManager {private final EntityManagerFactory emf;private final SupplierJpaController suppliers;public DBManager() {emf = Persistence.createEntityManagerFactory("OpenMapPU");suppliers = new SupplierJpaController(emf);}@Overridepublic List getSuppliers() {return suppliers.findSupplierEntities();}
}

@ServiceProvider(service = IDBManager.class)可以解决所有问题。 此行将DBManager添加到默认查找中。 在下一部分中,我们将从视图中了解如何访问DBManager 。 为了使其工作,如果NetBeans没有自动添加依赖项,则需要向org-openide-util-lookup.jar添加一个依赖项。

  1. 右键单击Libraries
  2. 从弹出菜单中选择添加JAR /文件夹...
  3. 导航到并选择<NetBeans_Installation>/platform/lib/org-openide-util-lookup.jar
  4. 选择复制到库文件夹 ,然后单击打开
  5. 清理并构建您的项目,以使更改生效。

但是什么是查找? 查找是将类对象作为键并将这些类对象的实例集作为值的映射,即

Lookup = Map<Class, Set<Class>> ,例如Map<String, Set<String>>Map<Provider, Set<Provider>> 。 NetBeans提供了许多访问默认查找的方法:

Provider provider = Lookup.getDefault().lookup(Provider.class); 
provider.aMethod();

或如果您具有Provider多个实现:

Collection providers = Lookup.getDefault().lookupAll(Provider.class); 
for (Provider provider : providers) { ... }

从上面的代码示例中可以看到,客户端不知道客户端使用哪种实现。 它只知道接口。 松耦合!

上面的代码将服务添加到默认查找中。 客户端在接口的默认查找中查找。 默认查找是一个评估META-INF/services文件夹中的服务声明的查找。 可通过Lookup.getDefault()方法调用它。 如果您对更多细节感兴趣,则Netbeans在build/classes/META-INF/services/文件夹内创建一个文本文件package.Provider ,其中包含实现类的完全限定名称。 通过以这种方式请求服务接口,您会收到在META-INF/services文件夹中注册的实现类的实例。

当然,在NetBeans胖客户端平台中还有其他查找,而不是默认查找,但是这些不在本文讨论范围之内。

4.5建立您的视图

最后,我们需要创建一个新图层,该图层将在地图上显示我们的供应商,并在openmap.properties中声明它,以便将其添加到地图中。 如果您遵循以前的教程,那么现在应该很容易做到。 让我们逐步构建SupplierLayer.java

清单9 – SupplierLayer.java

package openmap.controller;
public class SupplierLayer extends OMGraphicHandlerLayer {private static final String LOOKUP_OBJECT = "Lookup Object";public SupplierLayer() {// This is how to set the ProjectionChangePolicy, which// dictates how the layer behaves when a new projection is// received.setProjectionChangePolicy(new StandardPCPolicy(this, true));setRenderPolicy(new BufferedImageRenderPolicy());// Making the setting so this layer receives events from the// SelectMouseMode, which has a modeID of "Gestures". Other// IDs can be added as needed.setMouseModeIDsForEvents(new String[]{"Gestures"});}/*** Called from the prepare() method if the layer discovers that its* OMGraphicList is {@code null}.** @return new {@code OMGraphicList} with {@code OMGraphics{ that you always* want to display and reproject as necessary.*/public OMGraphicList init() {final IDBManager dbManager = Lookup.getDefault().lookup(IDBManager.class);final List suppliers = dbManager.getSuppliers();// This layer keeps a pointer to an OMGraphicList that it uses// for painting. It's initially set to null, which is used as// a flag in prepare() to signal that the OMGraphcs need to be// created. The list returned from prepare() gets set in the// layer.// This layer uses the StandardPCPolicy for new// projections, which keeps the list intact and simply calls// generate() on it with the new projection, and repaint()// which calls paint().OMGraphicList omList = new OMGraphicList();// Add suppliers as OMPoints.for (Supplier supplier : suppliers) {OMPoint omSupplier = new OMPoint(supplier.getLatitude(),supplier.getLongitude(), 3);  // radiusomSupplier.putAttribute(OMGraphicConstants.LABEL,new OMTextLabeler(supplier.getName(), OMText.JUSTIFY_LEFT));omSupplier.putAttribute(LOOKUP_OBJECT, supplier);omSupplier.setLinePaint(Color.BLUE);omSupplier.setSelectPaint(Color.ORANGE);omSupplier.setOval(true);omList.add(omSupplier);}return omList;}/*** This is an important Layer method to override. The prepare method gets* called when the layer is added to the map, or when the map projection* changes. We need to make sure the OMGraphicList returned from this method* is what we want painted on the map. The OMGraphics need to be generated* with the current projection. We test for a null OMGraphicList in the* layer to see if we need to create the OMGraphics. This layer doesn't* change it's OMGraphics for different projections, if your layer does, you* need to clear out the OMGraphicList and add the OMGraphics you want for* the current projection.** @return*/@Overridepublic synchronized OMGraphicList prepare() {OMGraphicList list = getList();// Here's a test to see if it's the first time that the layer has been// added to the map. This list object will be whatever was returned from// this method the last time prepare() was called. In this// example, we always return an OMGraphicList object, so if it's null,// prepare() must not have been called yet.if (list == null) {list = init();}/** This call to the list is critical! OMGraphics need to be told where* to paint themselves, and they figure that out when they are given the* current Projection in the generate(Projection) call. If an* OMGraphic's location is changed, it will need to be regenerated* before it is rendered, otherwise it won't draw itself. You generally* know you have a generate problem when OMGraphics show up with the* projection changes (zooms and pans), but not at any other time after* something about the OMGraphic changes.** If you want to be more efficient, you can replace this call to the* list as an else clause to the (list == null) check above, and call* generate(Projection) on all the OMGraphics in the init() method below* as you create them. This will prevent the* OMGraphicList.generate(Projection) call from making an additional* loop through all of the OMGraphics before they are returned.*/list.generate(getProjection());return list;}/*** Query that an OMGraphic can be highlighted when the mouse moves over it.* If the answer is true, then highlight with this OMGraphics will be* called.** @param omg* @return*/@Overridepublic boolean isHighlightable(OMGraphic omg) {return true;}/*** Query that an OMGraphic is selectable. Examples of handing selection are* in the EditingLayer. The default OMGraphicHandlerLayer behavior is to add* the OMGraphic to an OMGraphicList called selectedList. If you aren't* going to be doing anything in particular with the selection, then return* false here to reduce the workload of the layer.** @param omg* @return* @see com.bbn.openmap.layer.OMGraphicHandlerLayer#select* @see com.bbn.openmap.layer.OMGraphicHandlerLayer#deselect*/@Overridepublic boolean isSelectable(OMGraphic omg) {return true;}/*** Query for what tooltip to display for an OMGraphic* the mouse is over.** @param omg* @return*/@Overridepublic String getToolTipTextFor(OMGraphic omg) {String ttText = null;if (omg instanceof OMPoint) {OMPoint point = ((OMPoint) omg);Object attribute = point.getAttribute(OMGraphicConstants.LABEL);if (attribute != null && attribute instanceof OMTextLabeler) {OMTextLabeler labeler = (OMTextLabeler) attribute;ttText = labeler.getData();}}return ttText;}@Overridepublic Component getGUI() {JPanel panel = PaletteHelper.createPaletteJPanel("Suppliers Layer");JCheckBox chkShowLabels = new JCheckBox("Show/Hide Labels", true);chkShowLabels.addItemListener((ItemEvent e) -> {OMGraphicList omSuppliers = getList();for (OMGraphic omSupplier : omSuppliers) {if (chkShowLabels.isSelected()) {omSupplier.putAttribute(OMGraphicConstants.LABEL,new OMTextLabeler(((Supplier) omSupplier.getAttribute(LOOKUP_OBJECT)).getName(),OMText.JUSTIFY_LEFT));} else {omSupplier.removeAttribute(OMGraphicConstants.LABEL);}}repaint();});panel.add(chkShowLabels);return panel;}
}

init()方法中,我们使用默认的 LookupDBManager检索Supplier的列表。 我们遍历所有Supplier并从其中的每一个中创建一个OMPoint 。 通过设置属性OMGraphicConstants.LABEL创建点的标签。 通过使用键"Lookup Object"将支持的Supplier添加到OMPoint的属性映射中,我们可以实现一个技巧。 我们稍后将需要它。 (不要将其与NetBeans的Lookup混淆;我们只是以类似的方式命名它,以表明它类似于NetBeans的Lookup但与它无关;您可以将其命名为其他名称)。 最后,将每个点添加到返回的OMGraphicList

单击“ 工具”按钮时,“ 图层”对话框将调用getGUI()方法(请参见下图):

OpenMap5-Fig2 图2 –供应商层

该方法创建一个带有复选框的新面板,以显示/隐藏该图层的标签。 Supplier的标签是从OMPoint属性图检索的Supplier名称,以便为其设置OMGraphicConstants.LABEL属性。 选中复选框后,标签可见,否则属性被删除。 图层被repaint()编辑。 不幸的是, repaint()不能100%起作用。 您需要缩放地图或调整地图大小,以便再次显示标签。

接下来,当我们右键单击Supplier以显示其属性时,我们想显示一个弹出菜单。 从上一教程中,您知道我们需要重写getItemsForOMGraphicMenu()方法。 如果要在右键单击图层上的任何位置时显示弹出菜单,请重写以下方法getItemsForMapMenu()

清单10 – SupplierLayer.java(续)

@Override
public List getItemsForOMGraphicMenu(OMGraphic omg) {final OMGraphic chosen = omg;List menuItems = new ArrayList<>();JMenuItem mnuProperties = new JMenuItem("Properties")mnuProperties.addActionListener((ActionEvent ae) -> {//...});menuItems.add(mnuProperties);return menuItems;
}/*** This method is called mnuCreate a right mouse click is detected over the map* and not over an OMGraphic. You can provide a List of components to be* displayed in a popup menu. You have to do the wiring for making the list* components do something, though.** @param me* @return*/
@Override
public List getItemsForMapMenu(MapMouseEvent me) {List l = new ArrayList<>();JMenuItem mnuCreate = new JMenuItem("Create New Supplier");mnuCreate.addActionListener((ActionEvent ae) -> {fireRequestMessage("Create New Supplier");});l.add(mnuCreate);return l;
}

我们缺少显示数据的表格。 我们将在此处进行快速介绍,以向您展示另一个NetBeans向导,但是您可以使用Matisse或您所知道的自由构建自己的对话框。

  1. 创建一个新的包openmap.view.properties
  2. 右键单击它,然后选择新建→其他→Swing GUI表单→主/详细样本表单 ,然后单击下一步。
  3. 将其命名为SuppliersPropertiesDialogBox ,然后单击“ 下一步”。
  4. 选择您的数据库连接供应商表,然后从要包括列中排除SID字段。
  5. 点击完成

该向导已创建一个主/明细表格,但是我们只需要明细部分。 如下图所示对其进行自定义。

OpenMap5-Fig3 图3 –供应商属性对话框

选择每个文本字段和“ 删除”按钮,单击“ 绑定” (“ 属性”区域),然后从已enabled属性和text属性中删除对主表的任何引用。

OpenMap5-Fig4 图4 –删除绑定

类型文本字段更改为组合框,因为type仅限制为enum'GROSS''RETAIL'

  1. 右键单击组合框,然后选择“ Customize Code…
  2. 单击第二个默认代码组合框,然后将其更改为custom属性
  3. 将代码更改为以下代码,然后单击“ 确定”
cmbType.setModel(new javax.swing.DefaultComboBoxModel<>(Supplier.TYPE.values()));
  1. 再次单击组合框,然后在“ 属性”中单击“ 代码”
  2. 类型参数设置为<Supplier.TYPE>

在底部添加标签( lblStatus )。 使它不透明。 与数据库的事务处理成功后,将显示为绿色,否则显示为红色。 这对用户是一个很好的反馈,以确保他/她的修改得以保留。

该对话框与实体管理器耦合以检索要显示的数据,但这通常是一个不好的设计。 删除对实体管理器和主表的所有引用,并将其转换为JDialog 。 为了避免java.lang.IllegalArgumentException: GroupLayout can only be used with one Container at a time ,请将所有组件添加到JPanel

  1. 在“ 设计”视图中调整对话框的大小
  2. 将一个面板从面板拖到顶部(旋转容器)
  3. 将面板的变量名称更改为panel
  4. 将其布局设置为Free Design
  5. 导航器中选择所有小部件,然后将它们拖动到新面板中; 它们的布局应保持不变。
  6. 导航器中选择JDialog并将其大小属性设置为[400, 230] 。 取消选中“ 可调整大小”属性。

源代码应如下所示:

清单11 – SuppliersPropertiesDialogBox.java

public class SuppliersPropertiesDialogBox extends JDialog {private final Supplier supplier;private final IDBManager dbManager;public SuppliersPropertiesDialogBox(Supplier s) {dbManager = Lookup.getDefault().lookup(IDBManager.class);initComponents();supplier = s;setData(supplier);}@SuppressWarnings("unchecked")private void btnCloseActionPerformed(java.awt.event.ActionEvent evt) {this.setVisible(false);}private void btnDeleteActionPerformed(java.awt.event.ActionEvent evt) {try {dbManager.delete(supplier);lblStatus.setBackground(Color.green);} catch (Exception ex) {Logger.getLogger(SuppliersPropertiesDialogBox.class.getName()).log(Level.SEVERE, null, ex);lblStatus.setBackground(Color.red);}}private void btnSaveActionPerformed(java.awt.event.ActionEvent evt) {try {dbManager.save(getData());lblStatus.setBackground(Color.green);} catch (Exception ex) {Logger.getLogger(SuppliersPropertiesDialogBox.class.getName()).log(Level.SEVERE, null, ex);lblStatus.setBackground(Color.red);}}public void setData(Supplier supplier) {txtName.setText(supplier.getName());txtCity.setText(supplier.getCity());txtLatitude.setText(String.valueOf(supplier.getLatitude()));txtLongitude.setText(String.valueOf(supplier.getLongitude()));cmbType.setSelectedItem(supplier.getType());}public Supplier getData() {supplier.setName(txtName.getText());supplier.setCity(txtCity.getText());supplier.setLatitude(Double.valueOf(txtLatitude.getText()));supplier.setLongitude(Double.valueOf(txtLongitude.getText()));supplier.setType(Supplier.TYPE.valueOf(cmbType.getSelectedItem().toString()));return supplier;}// initComponents() generated method omitted ...
}

如您所见,我们引用DBManager来处理数据。 我们需要向其中添加以下新方法:

清单12 – IDBManager.java

public interface IDBManager {List getSuppliers();void delete(Supplier supplier) throws Exception;void save(Supplier supplier) throws Exception;
}

及其实现:
清单13 – DBManager.java

@Overridepublic void delete(Supplier supplier)  throws Exception {try {suppliers.destroy(supplier.getSid());} catch (NonexistentEntityException ex) {Logger.getLogger(DBManager.class.getName()).log(Level.SEVERE, null, ex);throw ex;}}@Overridepublic void save(Supplier supplier)  throws Exception {Supplier s = suppliers.findSupplier(supplier.getSid());if (s == null) {suppliers.create(supplier);} else {try {suppliers.edit(supplier);} catch (Exception ex) {Logger.getLogger(DBManager.class.getName()).log(Level.SEVERE, null, ex);throw ex;}}}

现在可以将SupplierLayer修改为:

清单14 – SupplierLayer.java

@Overridepublic List getItemsForOMGraphicMenu(OMGraphic omg) {List menuItems = new ArrayList<>();JMenuItem mnuProperties = new JMenuItem("Properties");mnuProperties.addActionListener((ActionEvent ae) -> {SuppliersPropertiesDialogBox dlgProperties =new SuppliersPropertiesDialogBox((Supplier)omg.getAttribute(LOOKUP_OBJECT));dlgProperties.setVisible(true);});menuItems.add(mnuProperties);return menuItems;}

最后,

  1. 清理并构建您的应用程序,然后运行它
  2. 单击“ 图层控件”按钮,使“ 世界城市”图层不可见; 这样,您的鼠标点击地图就不会选择城市,而是供应商
  3. 右键单击地图上的供应商,然后从弹出菜单中选择“ 属性 ”; 下图对话框将显示。 当您单击“ 保存”时,您获得绿色反馈,表明您的更改已成功保存。

OpenMap5-Fig5 图5 –供应商属性对话框

做得好! 您已经构建了大多数功能,并且您的设计允许您进行修改而无需更改所有层。

这是您可以尝试的TODO列表:

  • 将上述对话框中的纬度/经度文本字段设置为易于阅读的格式,即xxºyy'zzz"N|Sxxxºyy'zzz"E|W
    • 提示 :使用我们在上一篇文章中显示的DMSCoordInfoFormatter格式化纬度/经度双DMSCoordInfoFormatter值;
    • 您可以为纬度和经度的小时/分钟/秒使用单独的文本字段,以便用户可以轻松键入新值而不会弄乱特殊字符;
    • 保存更改后,确保OMPoint显示在其新位置; 你需要添加PropertyChangeListenerSupplierLayer监听在改变Supplier

    清单15 – SupplierLayer.java(续)

private final PropertyChangeListener listener = (PropertyChangeEvent evt) -> {if (evt.getPropertyName().equals("latitude") || evt.getPropertyName().equals("longitude")) {Supplier supplier = (Supplier) evt.getSource();OMGraphicList list = getList();for (OMGraphic omPoint : list) {if (omPoint.getAttribute(LOOKUP_OBJECT).equals(supplier)) {((OMPoint) omPoint).set(supplier.getLatitude(), supplier.getLongitude());break;}}repaint();}};public OMGraphicList init() {// ...// Add suppliers as OMPoints.for (Supplier supplier : suppliers) {// ...supplier.addPropertyChangeListener(listener);omList.add(omSupplier);}// ...}

为使以上各项起作用,您需要将Supplier转变为可观察的:

清单16 – Supplier.java(续)

public class Supplier implements Serializable {@Transientprivate final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);// ...public void setSid(int sid) {int oldSid = this.sid;this.sid = sid;changeSupport.firePropertyChange("sid", oldSid, sid);}// ...public void setName(String name) {String oldName = this.name;this.name = name;changeSupport.firePropertyChange("name", oldName, name);}// ...public void setCity(String city) {String oldCity = this.city;this.city = city;changeSupport.firePropertyChange("city", oldCity, city);}// ...public void setType(TYPE type) {TYPE oldType = this.type;this.type = type;changeSupport.firePropertyChange("type", oldType, type);}// ...public void setLatitude(double latitude) {double oldLatitude = this.latitude;this.latitude = latitude;changeSupport.firePropertyChange("latitude", oldLatitude, latitude);}// ...public void setLongitude(double longitude) {double oldLongitude = this.longitude;this.longitude = longitude;changeSupport.firePropertyChange("longitude", oldLongitude, longitude);}// ...public void addPropertyChangeListener(PropertyChangeListener listener) {changeSupport.addPropertyChangeListener(listener);}public void removePropertyChangeListener(PropertyChangeListener listener) {changeSupport.removePropertyChangeListener(listener);}
  • 添加拖动功能,即用户应该能够将地图上的供应商拖动到新位置
    • 使用上一篇文章中的提示;
    • 实现DrawingToolRequestor接口
    • findAndInit()定义并初始化DrawingTool的实例
    • 重写select()drawingComplete()方法
  • 添加创建新的供应商的功能(方法getItemsForMapMenu()SupplierLayer )。 应该显示SuppliersPropertiesDialogBox ,其中已经填充了用户在地图上单击的坐标的纬度/经度字段; 然后用户应填写其他字段,并将新的供应商添加到数据库中
  • 单击“ Drawing Tool Launcher按钮时,您可以在图层上添加许多类型的图形,而这可能不是您想要的。 由于我们希望我们的Supplier层仅显示OMPointompointloader像上一篇文章中所做的那样,修改openmap.componentsomdrawingtoolompointloader
  • 您可能会遇到的另一个问题是,当右键单击OMPoint ,将显示与通过getItemsForOMGraphicMenu()创建的弹出菜单不同的弹出菜单。 com.bbn.openmap.tools.drawing.OMDrawingTool包含dt.setBehaviorMask(OMDrawingTool.QUICK_CHANGE_BEHAVIOR_MASK)OMDrawingTool定义了许多行为掩码,如上一教程中所述。 作为解决方法,我们创建了自己的OMDrawingTool

结论

在本教程中,我们创建了一个三层独立应用程序,该应用程序使用JPA从关系数据库中检索数据,并将其显示为OpenMap的层。 我们看到了如何使用NetBeans Lookup API将视图与控制器松散耦合。

您应该已经对如何开发此类应用程序有所了解,但是请不要在实际的特别是关键的应用程序中使用此代码。 代码是错误的,既不高效也不是线程安全的(例如,有关如何从不同线程中的数据库检索数据的信息,请参见com.bbn.openmap.layer.location.LayerLocationLayer )。

您还可以使用其他技术来替换各个层,例如:

  • 用纯Java 8 lambda框架替换JPA来访问数据库( Speedment )
  • 使用DukeScript将JPA粘合到您的视图

例如,由于您的视图依赖于IDBManager而不是特定的实现(例如JPA的EntityManager ),因此它不受模型的任何更改的影响(只要SupplierIDBManager的方法不变)。 然后,您可以将JPA替换为Speedment,而无需更改视图。

如果时间和空间允许,我们可能会在以后的文章中进行调查。

参考资料

  1. OpenMap开发人员指南
  2. OpenMap开发人员提示
  3. 鲍尔等 等 (2016),《 Java Persistence with Hibernate》 ,第二版,Manning。
  4. Coehlo H.,Kiourtzoglou B., Java持久性API迷你书 , JavaCodeGeeks 。
  5. Epple T.(2009),“ NetBeans查找说明!”, DZone
  6. Epple T.(2016),“ JPA和Dukescript ”
  7. Goncalves A.(2013年),《 Java EE 7入门》,Apress。
  8. Keith M.和Schincariol M.(2013), Pro JPA 2 –精通Java™Persistence API ,第二版,APress。
  9. Kostaras I. 博客 ,“松耦合”

翻译自: https://www.javacodegeeks.com/2016/06/openmap-tutorial-5-3-tier-gis-application.html

pcl_openmap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/335669.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HH SaaS电商系统的商品系统设计

文章目录商品信息结构商品信息总结构商品信息结构图发布商品商品类型虚拟商品服务商品为什么服务商品要分类前端根据服务商品类型来设计不同的界面和交互订单状态和服务类型有关商品档案上下架运营商品上下架&#xff08;即商品营销上下架&#xff09;删除商品租户删除自供商品…

HH SaaS电商系统移动端商城,买家选择商品规格的交互设计

文章目录未完整选择商品规格时商品图如何显示库存如何显示价格如何显示选择规格提示文案如何显示原型图已选完整选择商品规格时商品图如何显示库存如何显示价格如何显示原型图SKU下架后如何显示原型图购买数量如何交互未完整选择商品规格时 商品图如何显示 显示SPU的主图&…

aws lambda_适用于无服务器Java开发人员的AWS Lambda:它为您提供了什么?

aws lambda无服务器计算如何帮助您的生产基础架构&#xff1f; 在过去的几年中&#xff0c;无服务器计算架构一直受到关注&#xff0c;因为它专注于应用程序的主要组件之一&#xff1a;服务器。 这种体系结构采用了不同的方法。 在下面的文章中&#xff0c;我们将解释无服务器…

HH SaaS电商系统的销售订单毛利润模块设计

HH SaaS电商系统的销售订单利润分析&#xff0c;仅是简单分析供运营人员参考而已&#xff0c;所以订单生成后就自动根据商品成本价和实际交易价生成利润表&#xff0c;而不需要等到订单归档后才生成。 销售子单的毛利润最终交易总额-成本总额销售子单的毛利率毛利润/最终交易总…

eclipse preference没有server_Java Web开发的前期准备工作,部署Tomcat服务器和Server环境创建...

Java Web: 顾名思义&#xff0c;就是用Java的方式来解决Web开发&#xff0c;我也不讲太多的理论问题&#xff0c;直接将如何上手来进行Java Web开发。那么&#xff0c;我们一般进行Java Web开发&#xff0c;可以使用的工具很多&#xff0c;我这里使用eclipse。选择eclipse的版本…

java int不将0忽略_Java微服务:蛋糕是骗人的,但您不能忽略它

java int不将0忽略构建微服务实际上意味着什么&#xff1f; 通过微服务框架的眼光回答 忽略微服务的趋势已变得不可能。 有些人会说这只是另一个难以忍受的流行语&#xff0c;而另一些人会背诵打破巨石的优势或采取逆势方法并关注负面因素。 在本文中&#xff0c;我们将全面了…

4怎么放大字体_Word字体怎么放大?简单教你几招轻松搞定

一般情况下我们都会选择用Word编辑文章。文章编辑完成之后我们通常会对文章进行排版。在排版的过程中如果我们需要将Word字体放大该怎么办呢&#xff1f;下面我就来教大家几个Word字体放大的小技巧&#xff0c;一起来看看吧。下面以放大“字体演示”为例为大家演示操作过程。一…

js splice坑_JavaScript的splice()方法在for循环中使用可能会遇到的坑!!!

在我们日常敲代码的时候&#xff0c;常常会用到splice()方法来删除数组中的元素(就是以截取的方式)&#xff0c;因为它会直接对数组进行修改。在使用splice之前&#xff0c;必备条件是&#xff0c;要先有一个数组。var arr new Array(1,2,3,4,5); // 初始化一个数组var delete…

wps如何将字体竖着排列_WPS文字中怎么竖着打字?wps文字竖排的设置方法介绍

wps中怎么样才能让文字竖着写啊&#xff1f;WPS现在的应用比较多&#xff0c;其实WPS文字和Word相差不过&#xff0c;都是一个办公软件&#xff0c;我们有时候在编辑的时候需要把文字竖排&#xff0c;那么&#xff0c;wps文字怎么竖排呢&#xff1f;接下来脚本之家小编就给大家…

HH SaaS电商系统的线上服务商品库存和采购设计

文章目录线上服务商品库存和采购整体思路线上服务商品的采购单状态线上服务商品的出库单状态商家完成服务线上服务商品库存和采购整体思路 线上服务商品直接在商品编辑页面编辑库存数量即可&#xff0c;服务端自动生成类型为“调整入库”的入库单和类型为“调整出库”的出库单…

arm linux gif 显示_100ASK_IMX6ULL arm板子如何显示图片、汉字、划线、背景色

最近在研究基于imx6ull开发板&#xff0c;想让开发板支持显示图片、字符串、背景色的功能。 操作的主要步骤如下&#xff1a;移植设备树和驱动移植libjpeg库编写测试程序一、移植设备树和驱动开发板原厂SDK已经移植了lcd对应的设备树和驱动。具体可以参考韦东山老师的文章https…

动态代码生成 静态代码生成_将速度提升到自己的个人代码生成器中

动态代码生成 静态代码生成Speedment是一个开源工具包 &#xff0c;可用于生成Java实体和管理器以与数据库进行通信。 如果您需要域模型的对象关系映射&#xff0c;那么这很好&#xff0c;但是在某些情况下&#xff0c;您可能希望使用数据库作为模板来生成完全不同的东西。 在本…

HH SaaS电商系统的库存调整单设计

为了更加灵活地变动商品的库存&#xff0c;以及其它原因导致系统库存数和实际库存数不一致&#xff0c;可以通过库存调整单进行校正。 简简单单设计&#xff0c;没有状态管理&#xff0c;不需要通过审核&#xff0c;直接变更库存数~ 原型设计 库存调整单列表 库存调整单详情…

mysql优化 运维_MySQL运维---MySQL优化

一、优化1、优化的角度2、优化的方向3、数据库优化思路4、MySQL优化介绍5、优化工具介绍二、操作系统优化1、top命令1)CPU2)内存3)slab分配器---Linux内存4)page cache5)swap分区修改成不使用swap分区&#xff1a;永久生效&#xff0c;修改配置文件&#xff1a;2、大页内存机制…

微基准测试 r_在您的构建过程中添加微基准测试

微基准测试 r介绍 作为一个行业&#xff0c;我们正在采用更高的透明度和更可预测的构建过程&#xff0c;以降低构建软件的风险。 持续交付的核心原则之一是通过反馈循环收集反馈。 在Dev9中 &#xff0c;我们采用了与CD原则一致的“ 先知道 ”原则&#xff0c;这意味着我们&…

HH SaaS电商系统服务商品在移动端下单结算的交互设计

服务商品只能单独下单购买&#xff0c;不允许加入购物车我们知道服务商品有三种形式&#xff1a;线上服务、到店服务、上门服务&#xff0c;因为不同服务形式买家和卖家所需的信息不同&#xff0c;所以交互界面也会不同&#xff0c;前端需要判断当前服务商品的服务形式&#xf…

linux 远程备份mysql数据库_使用脚本自动化远程备份MySQL数据库

通常情况下、MySQL都需要备份&#xff0c;备份的方法有很多种。下面是我用脚本配合计划任务完成的自动备份远程的数据库。一、 确认备份方案&#xff1a;备份机&#xff1a;ip192.168.8.51数据库服务器&#xff1a;ip192.168.8.46备份的内容&#xff1a;对mysql的studydb、cour…

javafx动画_JavaFX动画工具

javafx动画好的&#xff0c;我想是时候让您讲一个小秘密了。 最近三个月左右&#xff0c;我从事一个私人项目&#xff0c;目标是创建一个工具&#xff0c;使我可以轻松地为Java桌面应用程序创建动画。 JavaFX在API级别上提供了出色的动画支持&#xff0c;但对于初学者甚至中级程…

HH SaaS电商系统的商品类目设计

文章目录商品的基础类目创建基础类目编辑基础类目删除基础类目启用/禁用基础类目商城的营销类目新增商城营销类目编辑商城营销类目营销类目关联商品店铺的营销类目新增营销类目营销类目关联商品编辑店铺营销类目类目启用规则&#xff08;适用于全部类目&#xff09;商品类目分为…

HH SaaS电商系统的品牌模块设计

品牌和商品基础类目属于多对多的关系创建品牌时必须关联商品基础类目&#xff0c;且只能关联三级类目&#xff0c;至少关联一种品牌只能由租户统一进行维护管理&#xff0c;供应商、商家、商城可以申请新品牌&#xff0c;但是由租户进行审核品牌被删除或者停用后&#xff0c;关…